PUBLISHED JUNE 11, 2024
How do you deep clone an object in JavaScript?
Learn how to deep clone objects in JavaScript with various methods including JSON, recursion, structured cloning, and Lodash, including best practices and pitfalls.
Prerequisite
Before learning about deep cloning in JavaScript, you should have a basic understanding of:
- 1.JavaScript Objects: Creating and manipulating objects.
- 2.JavaScript Functions: Defining and invoking functions.
- 3.Data Types: Primitive (string, number) and reference (objects, arrays).
- 4.ES6 Features: Arrow functions, destructuring, and spread/rest operators.
- 5.JSON: Using
JSON.parse
andJSON.stringify
.
These basics will help you grasp deep cloning concepts effectively.
Introduction
In JavaScript, cloning an object means creating a new object with the same properties and values as an existing object.
This is often necessary when you want to make changes to an object without affecting the original one.
Cloning can be useful in many scenarios, such as copying configuration objects, managing state in applications, and avoiding unintended side effects.
Difference Between Shallow and Deep Cloning
Cloning can be categorized into two types: shallow cloning and deep cloning.
- ➢Shallow Cloning: This creates a new object and copies the references of the original object's properties to the new object.
If the properties are objects themselves, only the reference is copied, not the actual object.
This means changes to nested objects in the cloned object will affect the original object.
const originalOb = {
a: 1,
b: {
c: 2
}
};
const shallowCloneOb = Object.assign({}, originalOb);
shallowCloneOb.b.c = 3;
console.log(originalOb.b.c); // Output: 3
const original = {
name: "yohan",
age: 1,
address: {
city: "Douala",
zip: "00000"
}
};
// Perform shallow cloning using Object.assign
const shallowClone = Object.assign({}, original);
// Modify the nested object in the clone
shallowClone.address.city = "Alberta";
console.log(original.address.city); // Output: Alberta
console.log(shallowClone.address.city); // Output: Alberta
Just like in the first example, we bserve that changing the city
property in the shallowClone
also changes the city
property in the original
object because both address
properties refer to the same object.
- ➢Deep Cloning: This creates a new object and recursively copies all nested objects and their properties.
The new object is a completely independent copy of the original.
Changes to the deep-cloned object do not affect the original object.
const originalOb = {
a: 1,
b: {
c: 2
}
};
const deepCloneOb = JSON.parse(JSON.stringify(originalOb));;
deepCloneOb.b.c = 'c';
console.log(originalOb.b.c); // Output: 2
const original = {
name: "yohan",
age: 1,
address: {
city: "Douala",
zip: "00000"
}
};
// Perform shallow cloning using Object.assign
const deepClone = JSON.parse(JSON.stringify(original));
// Modify the nested object in the clone
deepClone.address.city = "Alberta";
console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta
Same result as in the previous example, changing the city
property in the deepClone
does not affect the city
property in the original
object because address
in the deepClone
is a completely new object, independent of the address
in the original
object.
Importance of Deep Cloning
Deep cloning is crucial when working with complex data structures where nested objects and arrays are involved.
It ensures that changes made to the cloned object do not impact the original object, maintaining data integrity and preventing side effects.
This is especially important in scenarios like:
- ➢State Management in Applications: When managing state, particularly in frameworks like `React` or `Vue`, deep cloning helps ensure that updates to state objects do not mutate the previous state unintentionally.
- ➢Immutable Data Structures: In functional programming paradigms, immutability is key. Deep cloning allows you to work with immutable data structures, ensuring that existing data remains unchanged.
- ➢Copying Complex Configurations: When working with configurations that contain nested objects or arrays, deep cloning ensures that modifications to the configuration do not affect the original settings.
1. JSON.parse and JSON.stringify
One of the simplest ways to perform a deep clone in JavaScript is by using the combination of JSON.parse
and JSON.stringify
.
This method converts an object into a JSON string and then parses the string back into a new object.
This approach effectively creates a deep copy of the original object.
Let's illustrate by revisit our example from above:
const original = {
name: "Yohan",
age: 1,
address: {
city: "Douala",
zip: "00000"
},
hobbies: ["Playing", "Crying"],
bornOn: new Date('2024-02-09')
};
// Deep clone using JSON methods
const deepClone = JSON.parse(JSON.stringify(original));
// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");
console.log(original.address.city); // Output: Wonderland
console.log(deepClone.address.city); // Output: Dreamland
console.log(original.hobbies); // Output: [ 'Playing', 'Crying' ]
console.log(deepClone.hobbies); // Output: [[ 'Playing', 'Crying', 'Eating' ]
console.log(original.bornOn, typeof original.bornOn); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.bornOn, typeof deepClone.bornOn); // Output: 2024-02-09T00:00:00.000Z string
Implications
- ➢Pros: Simple to use and understand.
- ➢Cons: Limited to JSON-compatible objects, can be slow for large objects.
- ➢Performance: Generally slower due to the serialization and deserialization process.
Limitations
While JSON.parse
and JSON.stringify
provide a simple way to deep clone objects, they have notable limitations:
- ➢Functions and Symbols: These methods do not handle functions or symbols, which are omitted during the cloning process.
- ➢Circular References: They cannot clone objects with circular references and will throw an error.
- ➢Special Data Types: This approach does not handle special data types like
Date
,Set
,Map
,RegExp
, andundefined
, as observed with the last twoconsole.log
statements.
2. Using Recursion
A more flexible approach to deep cloning involves writing a custom recursive function.
This method can handle many of the limitations of the JSON methods by explicitly checking and copying each property of the object, including nested objects.
function deepClone(obj, visited = new WeakMap()) {
if (obj === null || typeof obj !== "object") {
return obj;
}
if (visited.has(obj)) {
return visited.get(obj);
}
let clone;
if (Array.isArray(obj)) {
clone = [];
visited.set(obj, clone);
obj.forEach((item, index) => {
clone[index] = deepClone(item, visited);
});
}
else if (obj instanceof Date) {
clone = new Date(obj);
}
else if (obj instanceof RegExp) {
clone = new RegExp(obj);
}
else {
clone = {};
visited.set(obj, clone);
Object.keys(obj).forEach((key) => {
clone[key] = deepClone(obj[key], visited);
});
}
return clone;
}
const original = {
name: "Yohan",
age: 1,
address: {
city: "Douala",
zip: "00000"
},
hobbies: ["Crying", "Playing"],
birthDate: new Date("2024-02-09"),
pattern: /ab+c/
};
// Deep clone using custom recursive function
const deepCloneObj = deepClone(original);
// Modify the nested object and array in the clone
deepCloneObj.address.city = "Alberta";
deepCloneObj.hobbies.push("Eating");
console.log(original.address.city); // Output: Douala
console.log(deepCloneObj.address.city); // Output: Alberta
console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepCloneObj.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]
console.log(original.birthDate, typeof original.birthDate) // Outputs: 2024-02-09T00:00:00.000Z object
console.log(deepCloneObj.birthDate, typeof deepCloneObj.birthDate) // Outputs: 2024-02-09T00:00:00.000Z object
Implications
- ➢Pros: Highly customizable, can handle special data types.
- ➢Cons: Can be complex and error-prone, particularly with circular references.
- ➢Performance: Performance varies based on implementation; deep nested objects may result in stack overflow.
Benefit of Using a Custom Recursive Function
The custom recursive function above handles several edge cases such as:
- ➢Circular References: Managed using a
WeakMap
to track visited objects. - ➢Special Data Types: Includes support for
Date
andRegExp
.(as observed with last twoconsole.log
statements)
3. Structured Cloning Using structuredClone()
The structuredClone
method, available in modern browsers, is a built-in method for creating deep clones of objects.
It is designed to handle many of the limitations of other methods, including support for functions, symbols, and special data types.
const original = {
name: "Yohan",
age: 1,
address: {
city: "Douala",
zip: "12345"
},
hobbies: [ 'Crying', 'Playing' ],
birthDate: new Date("2024-02-09"),
pattern: /ab+c/
};
// Deep clone using structuredClone
const deepClone = structuredClone(original);
// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");
console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta
console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]
console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(original.pattern, typeof original.pattern); // Output: /ab+c/ object
console.log(deepClone.pattern, typeof deepClone.pattern); // Output: /ab+c/ object
This method is straightforward and highly efficient for deep cloning complex objects, making it a preferred choice when available.
Implications
- ➢Pros: Built-in support for many data types, handles complex structures well.
- ➢Cons: Limited to modern browsers and environments that support it.
- ➢Performance: Efficient for complex and large objects.
Using Third-Party Libraries
There are several third-party libraries available for deep cloning objects in JavaScript, each with its own advantages. Some popular ones include:
1. Lodash: a popular JavaScript utility library that provides a wide range of functions, including a robust cloneDeep
function for deep cloning objects. This method is well-tested and handles many edge cases effectively.
const _ = require('lodash');
const original = {
name: "Yohan",
age: 1,
address: {
city: "Douala",
zip: "12345"
},
hobbies: [ 'Crying', 'Playing' ],
birthDate: new Date("2024-02-09"),
pattern: /ab+c/
};
// Deep clone using Lodash's cloneDeep
const deepClone = _.cloneDeep(original);
// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");
console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta
console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]
console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(original.pattern, typeof original.pattern); // Output: /ab+c/ object
console.log(deepClone.pattern, typeof deepClone.pattern); // Output: /ab+c/ object
Lodash's cloneDeep
method is versatile and can handle complex objects with circular references, making it a reliable choice for deep cloning.
Implications
- ➢Pros: Robust, well-tested, handles various edge cases.
- ➢Cons: External dependency, larger bundle size.
- ➢Performance: Generally efficient, but performance can vary based on object complexity.
2. rfdc (Really Fast Deep Clone): A minimalistic and fast deep cloning library.
const rfdc = require('rfdc')();
const original = {
name: "Yohan",
age: 1,
address: {
city: "Douala",
zip: "12345"
},
hobbies: [ 'Crying', 'Playing' ],
birthDate: new Date("2024-02-09"),
pattern: /ab+c/,
greet: () => 'Dada'
};
// Deep clone using Lodash's cloneDeep
const deepClone = rfdc(original);
// Modify the nested object and array in the clone
deepClone.address.city = "Alberta";
deepClone.hobbies.push("Eating");
console.log(original.address.city); // Output: Douala
console.log(deepClone.address.city); // Output: Alberta
console.log(original.hobbies); // Output: [ 'Crying', 'Playing' ]
console.log(deepClone.hobbies); // Output: [ 'Crying', 'Playing', 'Eating' ]
console.log(original.birthDate, typeof original.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(deepClone.birthDate, typeof deepClone.birthDate); // Output: 2024-02-09T00:00:00.000Z object
console.log(original.greet, typeof original.greet); // Output: [Function: greet] function
console.log(deepClone.greet, typeof deepClone.greet); // Output: [Function: greet] function
3. deepcopy: A straightforward library that handles deep cloning with ease.
// some code goes here..
const deepClone = deepcopy(original);
// rest of code goes here ...
Install `deepclone` and substitute the line const deepClone = rfdc(original);
with const deepClone = deepcopy(original)
;
You should get the same result.
These third-party libraries provide simple, staright-forward, efficient and reliable deep cloning solutions, making them excellent choices for handling complex objects in JavaScript.
1. Circular References
Circular references occur when an object references itself directly or indirectly, creating a loop.
This can cause problems for many deep cloning methods, such as `JSON` methods, which will throw an error when encountering circular references.
Example of circular reference
const obj = {};
obj.self = obj; // Circular reference
console.log(JSON.parse(JSON.stringify(obj))) // TypeError: Converting circular structure to JSON
This can easily be resolved by using libraries like Lodash’s cloneDeep
or rfdc
which handle circular references out-of-the-box.
const _ = require('lodash');
const obj = {name: 'Yohan'};
obj.self = obj; // Circular reference
const clone = _.cloneDeep(obj);
console.log(clone); // output : <ref *1> { name: 'Yohan', self: [Circular *1] }
2. Cloning Complex Objects (Functions, DOM Elements, etc.)
Complex objects such as functions, DOM elements, and instances of custom classes often require special handling because standard deep cloning methods do not support them out-of-the-box.
- ➢Cloning Functions: Functions cannot be deep-cloned using JSON methods or even many deep clone libraries. They need to be copied explicitly.
const originalObj = {
greet: function() {
return 'Hello! I\'m Yohan';
}
};
// Custom function cloning
function cloneFunction(func) {
const funcStr = func.toString();
return new Function(`return (${funcStr})`)();
}
const clonedObj = {};
clonedObj.greet = cloneFunction(originalObj.greet);
console.log(clonedObj.greet()); // Outputs: "Hello! I'm Yohan!"
- ➢Cloning DOM Elements: DOM elements can be cloned using the
cloneNode
method.
// Assuming we have an html file containing an element with id myElement
const originalElement = document.getElementById('myElement');
const clonedElement = originalElement.cloneNode(true); // true for deep clone
document.body.appendChild(clonedElement);
- ➢Cloning Instances of Custom Classes: Instances of custom classes require recreating the instance with the same properties
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
}
}
// Custom class instance cloning
function cloneInstance(instance) {
const clone = Object.create(Object.getPrototypeOf(instance));
Object.assign(clone, instance);
return clone;
}
const originalPerson = new Person('Yohan', 1);
const clonedPerson = cloneInstance(originalPerson);
console.log(clonedPerson.greet()); // Outputs: "Hello, my name is Yohan and I am 1 years old."
Conclusion
Deep cloning objects in JavaScript is a crucial operation for creating independent copies of complex data structures.
In this guide, we explored various methods and considerations for deep cloning, including:
- ➢Understanding the difference between shallow and deep cloning.
- ➢Exploring different methods for deep cloning, such as JSON methods, recursive functions, structured cloning, and third-party libraries like Lodash.
- ➢Considering performance implications, memory management, and best practices for choosing the right method.
- ➢Addressing common pitfalls like circular references and cloning complex objects such as functions, DOM elements, and custom classes.
Recommendations
- ➢Choose the Right Method: Consider the complexity of your objects, performance requirements, and environment constraints when selecting a deep cloning method.
- ➢Handle Circular References: Be aware of circular references in your objects and use methods or libraries that handle them gracefully.
- ➢Test Thoroughly: Test your deep cloning implementations thoroughly, especially with complex and nested objects, to ensure correctness and performance.
Deep cloning is a fundamental concept in JavaScript development, and mastering it allows developers to work with complex data structures more effectively.
By understanding the methods, considerations, and best practices outlined in this article, you can confidently tackle deep cloning challenges in their projects.
Happy coding💻🤓!