Shallow Copy VS Deep Copy
Nov 23, 2022
Recently I was asked about copying JavaScript objects. It made me realize I didn't deeply understand some of the core concepts of this fundamental JavaScript topic.
Since nearly everything in JavaScript is an object except for the primitives (boolean, null, undefined, string, number and symbol), we can say that objects are the foundation of JavaScript and permeate its every aspect.
Although copying seems straightforward, a lot of things can go wrong if you use a naive approach. In fact there are two different ways, known as shallow copy and deep copy.
I want to discuss them in this article but before starting, a key concept to understand is mutability.
Unlike primitives data types, objects are mutable, this means that they can be modified after their declaration. However, it’s not good to work in a mutable fashion.
The main benefits of immutability are predictability and performance: it’s easier to debug code that doesn’t hide changes and it’s also easy to see if anything has changed at all.
With regard to performance, immutable Objects can make use of structural sharing to reduce memory usage.
So now you might wonder: okay, but what is the difference between a shallow copy and a deep copy?
A shallow copy actually doesn’t make a new copy of the object in memory. It only copies a reference to the original object.
This means that if we add or change the values in the new object, we are actually going to mutate the source object.
A deep copy on the other hand is a way to make a brand new object that is disentangled from the original one.
Shallow Copy
Let’s consider some practical examples:
let cat = {
name: 'Oli',
color: 'Black'
};
let copyCat = cat;
In the code above we are using the assignment operator to make a new object called copyCat. As explained, however, we are only making a reference to the cat object.
Let’s see what happens if we change the value of color in copyCat from black to red:
copyCat.color = "Red";
console.log(cat)
// output: {"name":"Oli","color":"Red"}
The original cat object color mutated and the output says it is red now.
Other examples of shallow copy:
Spread Operator and Object.assign()
Consider the following snippet:
let cat = {
name: 'Oli',
color: 'Black',
passions: {
food: "salmon",
activity: "sleeping",
place: "garden"
}
};
The first method I want to show you is the spread operator:
let copyCat = {...cat}
copyCat.name = "Silvester"
copyCat.passions.food = "carrots"
console.log(cat)
//output: {"name":"Oli","color":"Black","passions"{"food":"carrots","activity":"sleeping","place":"garden"}}
This time we changed the name value and also the food value inside the nested object.
As you can see, when we tried to print the original cat object, only the value “carrots” inside the nested object mutated, while our cat name is still Oli.
This happens because the spread operator makes a new copy of the all the key-value pairs, but it only creates a reference to the nested object.
The second method is Object.assign():
let copyCat = Object.assign({}, cat)
If we replace the spread operator with the line of code above, the output gives us the exact same result we got spreading the object.
Disclaimer: there are some differences between these two methods, but I chose to not talk about them here to keep the explanation simple. If you are interested in learning about the differences you can visit this link to the MDN documentation
Deep copy using JSON methods
Creating a deep copy can be trickier than creating shallow copies.
One of the easiest way is to use JSON.stringify() to convert the object to a JSON string, and then JSON.parse() to convert the string back into a completely new JavaScript object like in the following example:
let cat = {
name: 'Oli',
color: 'Black',
passions: {
food: "salmon",
activity: "sleeping",
place: "garden"
}
};
let copyCat = JSON.parse(JSON.stringify(cat));
copyCat.passions.food = "carrots"
console.log(cat)
//output: {"name":"Oli","color":"Black","passions"{"food":"salmon","activity":"sleeping","place":"garden"}}
Our old cat still likes salmon. The original nested object is unchanged as well as all the other values.
This works because in order to create the Json string, the stringify method goes through all the properties and also the nested objects.
However you should keep in mind that this method only work with simple objects (Serializable objects).
If an objects contains functions, properties with undefined values, Symbols and many other cases these Json methos cannot be applied.
For this reason is worth mentioning that this is not the most perfomant way to make a deep copy.
For complex objects, it may be a better idea to make a custom function that iterate through the different components within and apply different methods to each component depending on what type of thing it is.
This helps break the problem down into smaller tasks, rather than trying to copy the entire thing at once.
Conclusions
In this article I explained some of the methods to make copies of an object and the reasons why it might be a good idea to use deep copies over shallow copies to preserve the concept of immutability in your code.
Nonetheless we saw how some methods in the shallow copy category actually work really well at making a new copy and not references when our source object doesn’t contain any other nested object. Therefore I suggest you assess which method is best suited for your needs.