Deep Dive into Cloning Objects in JavaScript

Deep Dive into Cloning Objects in JavaScript

In JavaScript, objects are reference types, meaning when you assign an object to another variable, you’re assigning the reference to that object, not the object itself. If you modify the object through the new reference, the original object will also reflect the changes. To avoid this, cloning (or copying) an object is necessary. Let’s explore different ways to clone objects and understand their behavior.

Shallow Cloning vs. Deep Cloning

  1. Shallow Cloning: Only the immediate properties of the object are cloned. If the object contains other objects (nested), those nested objects are not cloned but copied by reference.
  2. Deep Cloning: This type of cloning duplicates everything, including nested objects. Each level of nested structure gets cloned, creating completely independent copies.

Methods of Cloning in JavaScript

1. Using Object.assign() (Shallow Cloning)

Object.assign() is commonly used for shallow cloning an object. It copies the properties of one or more source objects to a target object.

const obj1 = { 
  name: 'Alice', 
  age: 25, 
  address: { city: 'New York', zip: 10001 } 
};

const clonedObj = Object.assign({}, obj1);

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';

console.log(obj1); // { name: 'Alice', age: 25, address: { city: 'Los Angeles', zip: 10001 } }
console.log(clonedObj); // { name: 'Bob', age: 25, address: { city: 'Los Angeles', zip: 10001 } }
  • Explanation:
  • Object.assign() only creates a shallow clone. In this case, the address object inside obj1 is still referenced in clonedObj. Hence, changes to address.city affect both objects.
  • However, primitive properties like name are copied independently, as seen by the difference between obj1.name and clonedObj.name.

2. Using Spread Operator {...} (Shallow Cloning)

The spread operator is another way to perform shallow cloning in JavaScript.

const obj1 = { 
  name: 'Alice', 
  age: 25, 
  address: { city: 'New York', zip: 10001 } 
};

const clonedObj = { ...obj1 };

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';

console.log(obj1); // { name: 'Alice', age: 25, address: { city: 'Los Angeles', zip: 10001 } }
console.log(clonedObj); // { name: 'Bob', age: 25, address: { city: 'Los Angeles', zip: 10001 } }
  • Explanation:
  • Similar to Object.assign(), the spread operator { ...obj1 } creates a shallow copy. Changes in nested objects (like address) still affect both the original and cloned objects.

3. Using JSON.stringify() and JSON.parse() (Deep Cloning)

One way to perform deep cloning is by converting an object into a JSON string and then parsing it back into an object.

const obj1 = { 
  name: 'Alice', 
  age: 25, 
  address: { city: 'New York', zip: 10001 } 
};

const clonedObj = JSON.parse(JSON.stringify(obj1));

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';

console.log(obj1); // { name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 } }
console.log(clonedObj); // { name: 'Bob', age: 25, address: { city: 'Los Angeles', zip: 10001 } }
  • Explanation:
  • This method creates a deep clone, so changes in nested objects (like address) do not affect the original object.
  • Limitation: It only works for JSON-serializable data. For instance, it doesn’t handle functions, undefined, or Symbol properties. Circular references (where an object references itself) will also throw an error.

4. Using a Recursive Function (Deep Cloning)

For more control, you can write a recursive function to deep clone objects, handling nested structures.

function deepClone(obj) {
  if (obj === null || typeof obj !== 'object') {
    return obj;
  }

  const clone = Array.isArray(obj) ? [] : {};

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      clone[key] = deepClone(obj[key]);
    }
  }

  return clone;
}

const obj1 = { 
  name: 'Alice', 
  age: 25, 
  address: { city: 'New York', zip: 10001 }, 
  hobbies: ['reading', 'traveling']
};

const clonedObj = deepClone(obj1);

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';
clonedObj.hobbies.push('coding');

console.log(obj1); // { name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: ['reading', 'traveling'] }
console.log(clonedObj); // { name: 'Bob', age: 25, address: { city: 'Los Angeles', zip: 10001 }, hobbies: ['reading', 'traveling', 'coding'] }
  • Explanation:
  • This method recursively clones each property of the object. It handles nested arrays and objects properly, creating a deep copy without altering the original structure.

5. Using structuredClone() (Deep Cloning)

The structuredClone() function is a built-in modern API for deep cloning objects. It provides a robust and efficient way to deep clone an object and supports complex data types such as Map, Set, ArrayBuffer, and others.

const obj1 = {
  name: 'Alice',
  age: 25,
  address: { city: 'New York', zip: 10001 },
  hobbies: ['reading', 'traveling'],
  map: new Map([['key', 'value']]),
  set: new Set([1, 2, 3])
};

const clonedObj = structuredClone(obj1);

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';
clonedObj.hobbies.push('coding');
clonedObj.map.set('key', 'newValue');

console.log(obj1);
// { 
//   name: 'Alice',
//   age: 25,
//   address: { city: 'New York', zip: 10001 },
//   hobbies: ['reading', 'traveling'],
//   map: Map { 'key' => 'value' },
//   set: Set { 1, 2, 3 }
// }

console.log(clonedObj);
// { 
//   name: 'Bob',
//   age: 25,
//   address: { city: 'Los Angeles', zip: 10001 },
//   hobbies: ['reading', 'traveling', 'coding'],
//   map: Map { 'key' => 'newValue' },
//   set: Set { 1, 2, 3 }
// }
  • Explanation:
  • structuredClone() creates a deep copy of the object, supporting complex data types like Map, Set, and ArrayBuffer. It’s a reliable way to deep clone in modern JavaScript, handling more scenarios than JSON.stringify()/JSON.parse().
  • Limitation: This method does not support cloning of objects with functions, or certain platform-specific objects like DOM elements.

6. Using Libraries for Deep Cloning

For larger projects, using a well-tested library might be the most efficient solution. Libraries like Lodash provide a deep cloning function _.cloneDeep() that handles complex cases such as circular references and non-enumerable properties.

const _ = require('lodash');

const obj1 = { 
  name: 'Alice', 
  age: 25, 
  address: { city: 'New York', zip: 10001 },
  hobbies: ['reading', 'traveling']
};

const clonedObj = _.cloneDeep(obj1);

clonedObj.name = 'Bob';
clonedObj.address.city = 'Los Angeles';
clonedObj.hobbies.push('coding');

console.log(obj1); // { name: 'Alice', age: 25, address: { city: 'New York', zip: 10001 }, hobbies: ['reading', 'traveling'] }
console.log(clonedObj); // { name: 'Bob', age: 25, address: { city: 'Los Angeles', zip: 10001 }, hobbies: ['reading', 'traveling', 'coding'] }
  • Explanation:
  • Lodash’s cloneDeep() method handles deep cloning efficiently, even with more complex object structures. It’s widely used in production-level code to avoid edge cases.

Conclusion

  • Shallow Cloning: Use Object.assign() or the spread operator {...} if you only need to clone the top-level properties of an object. This is efficient for simple objects without deeply nested structures.
  • Deep Cloning:
  • Use JSON.stringify()/JSON.parse() for basic deep cloning. This method works well with plain objects that only contain serializable data like strings, numbers, arrays, and objects.
  • Use structuredClone() when you need a more robust solution that handles modern data types such as Map, Set, ArrayBuffer, and avoids common pitfalls of the JSON.stringify() method. This is a simple and native API in modern JavaScript for deep cloning without external libraries.
  • Use Libraries like Lodash for production-level deep cloning, especially when dealing with more complex scenarios like circular references, functions, or non-enumerable properties.
  • Write a Recursive Function if you need a customizable solution and want control over the cloning process.

By understanding these cloning methods, you can avoid unintended side effects when working with objects in JavaScript. Choose the right approach based on your specific use case—whether you need to modify only the outer structure or make a complete copy of all nested data.


Comparison Table of Cloning Methods

MethodShallow or Deep?Handles Nested Objects?Supports Complex Types (Map, Set, etc.)Handles Functions?Notes
Object.assign()ShallowNoNoNoQuick shallow copy, only for simple objects.
Spread Operator {...}ShallowNoNoNoAnother quick way to shallow clone objects.
JSON.stringify()/parse()DeepYesNoNoOnly works with JSON-serializable data.
structuredClone()DeepYesYesNoModern API for deep cloning with better support.
Recursive FunctionDeepYesCustomizableCustomizableCustom solution to control every cloning scenario.
_.cloneDeep() (Lodash)DeepYesYesYesProduction-ready, handles complex objects.

This table summarizes each method’s strengths and limitations. Let me know if you’d like to adjust or add any more content!