In JavaScript, objects are stored in memory like reference, so you can't duplicate them just by using =. Previously, you learned how to copy objects. But when it comes to nested objects, the process becomes more complicated: they can have another object, array, or function (method) as a property. Let's figure out how you can clone this type of objects.
Nested objects
Properties in objects can be not only primitives but also references to other objects.
Like this:
const rectangle = {
color: "red",
sizes: {
height: 30,
width: 15
}
};
console.log(rectangle.sizes.height ); // 30
This type of object is called a nested object because it has a property sizes, which is also an object. If you try to clone this object using Object.assign() or a spread operator, this property will be copied by reference, so cloneRectangle and rectangle will share the same sizes:
const rectangle = {
color: "red",
sizes: {
height: 30,
width: 15
}
};
const cloneRectangle = Object.assign({}, rectangle);
console.log( rectangle.sizes === cloneRectangle.sizes ); // true, same object
// rectangle and cloneRectangle share sizes
rectangle.sizes.width = 25; // change a property from one place
console.log(cloneRectangle.sizes.width); // 25, get changed result from the other one
To fix that and make rectangle and cloneRectangle truly separate objects, it's best to use a cloning loop that examines each value of rectangle[key] and, if it's an object, replicates its structure as well. That is called deep cloning.
Using JSON
Using the JSON.stringify() and JSON.parse() methods allows for making deep copies of objects. A deep copy of an object will have properties that don't share the same references as the source object from which the copy was made. As a result, you can change one object, and it will not cause any unexpected changes to neither the source nor the copy.
First, the JSON.stringify() function converts the object into a JSON string. Then, the JSON.parse() method parses the string into a new object:
const man = {
name: 'John',
age: 28,
mother: { name: 'Kate', age: 50 }
};
console.log(JSON.stringify(man)); // '{"name":"John","age":28,"mother":{"name":"Kate","age":50}}'
// This is a JSON string
const deepCloneMan = JSON.parse(JSON.stringify(man));
// JSON.parse converts JSON string into an object
console.log(deepCloneMan);
/*
{
name: 'John',
age: 28,
mother: { name: 'Kate', age: 50 }
}
*/
Now you have two independent objects, and the property mother is not a reference. Here is what it means:
// above code is used here
console.log(man === deepCloneMan); //false
console.log(man.mother === deepCloneMan.mother); //false
deepCloneMan.mother.age = 45;
man.name = 'Jack';
console.log(man);
/*
{
name: 'Jack',
age: 28,
mother: { name: 'Kate', age: 50 }
}
*/
console.log(deepCloneMan);
/*
{
name: 'John',
age: 28,
mother: { name: 'Kate', age: 45 }
}
*/
We did it! Now you can clone objects and get absolutely independent new variables.
This method is the most popular and fastest way to create a deep copy of a value. However, it has a couple of shortcomings. For example, JSON.stringify() just ignores functions, and the object remains without such properties. The same is true for some other more complex and less-used things (like recursive data structures and built-in types). For example:
const counter = {
count: 1,
increaseCount: function(){
this.count += 1
return this.count
}
}
console.log(JSON.stringify(counter)) // {"count": 1}
// functions are ignored
Also, JSON.stringify() converts some unsupported data types to supported ones. This is what happens in cases like these:
JSON.stringify({ key: NaN });
JSON.stringify({ key: Infinity });
// all will be converted to '{"key": null}'
Finally, there is an issue with Date(). Dates will be parsed as Strings, not as Dates. So, if you store a Dates object in your object and clone it using JSON, you will lose them, too:
// You can check it in the console
const date = new Date();
console.log(date); // Sat Mar 11 2023 13:51:30 GMT+0300 ((Standard Time))
console.log(typeof date); // 'object'
const copiedDate = JSON.parse(JSON.stringify(date));
console.log(copiedDate); // '2023-03-11T10:51:30.814Z'
console.log(typeof copiedDate); // 'string'
Finally, let's have a look at one last, most recent method.
StructuredClone
It is a new global function released in 2022. Node.js v17.0.0 supports it, and also available in Deno, Firefox, Chrome, Edge, and Safari web browsers. The call structuredClone(object) clones the object with all nested properties.
const user = {
name: 'Chandler',
surname: 'Bing',
friends: [ 'Monica', 'Ross', 'Rachel' ]
};
const cloneUser = structuredClone(user);
console.log(cloneUser);
/*
{
name: 'Chandler',
surname: 'Bing',
friends: [ 'Monica', 'Ross', 'Rachel' ]
}
*/
Structured cloning addresses many (but not all) shortcomings of the JSON.stringify() technique. Structured cloning can handle cyclical data structures, support many built-in data types, and is generally more robust and often faster. But, like JSON.stringify(), this method also doesn't support cloning functions.
structuredClone({
f: function() {}
}); // error
To handle such complex cases, you may need to use a combination of cloning methods or some JS libraries, for instance, lodash that has the cloneDeep(object) for this purpose.
Conclusion
Objects are assigned and copied by reference. Even if they are properties of other objects, they are stored as a "reference" (address in memory) for the value. So, to copy nested objects it's not enough to do shallow copying — this process requires deep cloning methods. The JSON.parse(JSON.stringify(object)) and structuredClone(object) methods are used for deep cloning of objects, especially nested objects.