In a previous post I talked about how to clone an object in JavaScript. The premise of that post was, when you have an object in JS and you try to copy it like you can with a string, number or boolean literal by just assigning it to a new variable, Javascript copies a reference to that object into the new variable instead of copying the object itself. After that, if you change a property of that object in one variable, that property will change in the other variable too, because they are both pointing to the same object.
The methods of copying an object that I outlined in that post do clone the object instead of copying a reference to it, however they do what is known as a shallow copy. That is, if the object has other nested objects in it, those objects are copied by reference.
Nested Objects
To make my person object example from the other post more useful, we might want to split the address component up into multiple parts so we can use it in multiple lines, or run analytics over the smaller units of data it would provide. It could look like this:
const person1 = {
age: 35,
name: 'Jen',
address: {
street: '1 Martin Pl',
city: 'Sydney',
state: 'NSW',
postcode: '2000',
}
}
If I clone this object with one of the methods I suggested previously (we’ll use the spread operator because it’s cool!) and then change a few of the values in one of the objects, this is what happens:
const person2 = { ...person1 }; // spread operator makes a copy of the object
person2.name = 'Julie';
person2.address.city = 'Brisbane';
In the above, I’ve used dot notation to alter person 2’s name and city. Now I’ll log them to the console:
console.log(person1);
console.log(person2);
{age: 35, name: 'Jen', address: {…}}
address: {street: '1 Martin Pl', city: 'Brisbane', state: 'NSW', postcode: '2000'}
age: 35
name: "Jen"
{age: 35, name: 'Julie', address: {…}}
address: {street: '1 Martin Pl', city: 'Brisbane', state: 'NSW', postcode: '2000'}
age: 35
name: "Julie"
We can see that the name property of each object can be changed independently of the other, because it is a true copy, but the city property in the nested address object is Brisbane for both. That’s because the address object inside the two person objects is a reference to the same object.
JS doesn’t drill down into the nested objects and clone them as well. That’s why it’s called a shallow copy – because the copying only occurs on the surface level members of the object. Anything nested is copied by reference.
So how do we do it, if we want to fully clone the entire object?
How to Deep Clone
A copy that is not shallow is called a deep clone. You could write your own custom function to copy all the nested objects in an object, but it’s tricky and involved and there are easier ways of doing it.
It’s possible to do it with a combination of JSON.stringify and JSON.parse like this:
const person3 = JSON.parse(JSON.stringify(person1)); // stringify & parse combo to deep clone object
person3.name = 'Julie';
person3.address.city = 'Brisbane';
This does the job – when I log p1 and p3 to the console, you can see that the address property of p3 has been changed independently of the address property of p1. Jen still lives in Sydney and Julie’s moved to Brisbane, (albeit they somehow still live at the same street address 😅 )
{age: 35, name: 'Jen', address: {…}}
address: {street: '1 Martin Pl', city: 'Sydney', state: 'NSW', postcode: '2000'}
age: 35
name: "Jen"
{age: 35, name: 'Julie', address: {…}}
address: {street: '1 Martin Pl', city: 'Brisbane', state: 'NSW', postcode: '2000'}
age: 35
name: "Julie"
Why does that work? Well it’s because when I JSON.stringify an object it turns it into a string. If I just run the stringify portion of the code I get a string:
const person3 = JSON.stringify(person1);
console.log(person3);
>{"age":35,"name":"Jen","address":{"street":"1 Martin Pl", "city":"Sydney", "state":"NSW", "postcode":"2000"}}
When I run JSON.parse over it, it creates a whole new object from that string. So it works to duplicate nested objects. But it has some limitations. If I add a method to the object, and stringify it, for example:
const person1 = {
age: 35,
name: 'Jen',
address: {
street: '1 Martin Pl',
city: 'Sydney',
state: 'NSW',
postcode: '2000',
},
getShippingCost: function(){
console.log('api call to figure out shipping cost');
}
}
console.log(JSON.stringify(person1));
>{"age":35,"name":"Jen","address":{"street":"1 Martin Pl", "city":"Sydney", "state":"NSW", "postcode":"2000"}}
it doesn’t convert the function. Another thing to note is that if the object contains any dates, stringify will convert those dates into strings, and when the JSON.parse converts it back to an object, it won’t know that the dates are dates, and therefore in the new object, those dates will remain strings.
Lodash – creating a deep copy with a third party library
Probably the easiest way to get a deep copy of an object is to let a library do the heavy lifting for you. Lodash is a library that has loads of cool functions for applying set theory (intersect, difference, union etc) to arrays, iterating objects, arrays and strings and lots more. You can see the enormous amount of functionality that it offers here.
The function we want to use from the library is called cloneDeep and it deep copies an object including its methods and also maintains the data types in the resulting copy.
You can get the source code and load it in the browser using a script tag, or just use a CDN which I’m going to do, so in my index.html I have added this before the closing body tag:
<script src="https://cdn.jsdelivr.net/npm/lodash@4.17.21/lodash.min.js"></script>
NOTE: I’m doing this in the browser because it’s easy to demonstrate, but of course you can install Lodash using npm and use it in Node or React.
In my JS file I can call a Lodash functions by using _ (an underscore, or, literally, low dash). Now, in a single line of code, I can deep clone the person1 object, and all the nested objects are fully copied and the methods and datatypes are preserved in the clone:
const person1 = {
age: 35,
name: 'Jen',
address: {
street: '1 Martin Pl',
city: 'Sydney',
state: 'NSW',
postcode: '2000',
},
joiningDate: new Date(), // <- date type that needs to be preserved
getShippingCost: function(){ // <- method that needs to be cloned
console.log('api call to figure out shipping cost');
}
}
const person2 = _.cloneDeep(person1); // <- using _ to call lodash function
// vv changing some values to make sure the nested objects are fully cloned
person2.name = 'Julie';
person2.address.city = 'Brisbane';
The output when I log it to the console looks like this:
console.log(person1);
console.log(person2);
{age: 35, name: 'Jen', address: {…}, joiningDate: Tue May 31 2022 11:14:07 GMT+1000 (Australian Eastern Standard Time), getShippingCost: ƒ}
address:
city: "Sydney"
postcode: "2000"
state: "NSW"
street: "1 Martin Pl"
age: 35
getShippingCost: ƒ ()
joiningDate: Tue May 31 2022 11:14:07 GMT+1000 (Australian Eastern Standard Time) {}
name: "Jen"
{age: 35, name: 'Julie', address: {…}, joiningDate: Tue May 31 2022 11:14:07 GMT+1000 (Australian Eastern Standard Time), getShippingCost: ƒ}
address:
city: "Brisbane"
postcode: "2000"
state: "NSW"
street: "1 Martin Pl"
age: 35
getShippingCost: ƒ ()
joiningDate: Tue May 31 2022 11:14:07 GMT+1000 (Australian Eastern Standard Time) {}
name: "Julie"
We can see that everything has been fully cloned, the date is still a date in the cloned object and the function has been copied over too! Lodash makes it super easy to deep clone an object, a job which is otherwise pretty hard in vanilla JS.