Understanding of Deep Copy and Shallow Copy in JavaScript

Have you ever copied an object in Javascript, only to find out the changes in the new object also affect the original? But you clearly made a copy, so why does the original still get affected?
This is why understanding the concept of deep copy and shallow copy is crucial when working with non-primitive values in Javascript.
JavaScript treats primitive values (like numbers, strings, and booleans) and non-primitive values (like objects and arrays) differently when copying them. When you copy a primitive value, it creates a new value
const original = "Kannan";
let copied = original;
copied = "Ravindran";
console.log(original) // "Kannan"
console.log(copied) // "Ravindran"
In the above example, original
value remains unchanged even after modifying thecopied
value. This is because the copied
value is an entirely new value.
But when you copy a non-primitive value, it copies the value by reference. That means a new variable points to the same memory location as the original. This creates a connection between the original and duplicate values.
const original = {
firstname: 'Kannan',
lastname: 'Kannan'
};
let copied= original;
copied.lastname = 'Ravindran';
console.log(original.lastname); // "Ravindran"
console.log(copied.lastname); // "Ravindran"
In the above example, the original
value is changed even though you only modified the copied
value. This is because we performed a shallow copy.
We expect the original
value to remain unchanged, but it gets modified. This can lead to unexpected bugs in your code.
Spread Operator:
You may have heard about the spread operator. Introduced in ES2015, it’s simple, concise, and elegant. As the name suggests, it “spreads” the values of an object or array into a new one
const original = {
firstname: 'Kannan'
};
const copied = { ...original } // spread operator
copied.firstname = "Ravindran";
console.log(original.firstname); // "Kannan"
console.log(copied.firstname); // "Ravindran"
As you can see, the original
value remains unchanged. So, is this a deep copy? Not so fast. Let’s look at another example.
const original = {
firstname: 'Kannan',
moreDetails: {
lastname: 'Kannan'
}
};
const copied = { ...original } // spread operator
copied.moreDetails.lastname = "Ravindran"
console.log(original.moreDetails.lastname); // "Ravindran"
console.log(copied.moreDetails.lastname); // "Ravindran"
Just like before, we copied the object using the spread operator and modified the lastname
in the copied object. However, this time, the lastname
in the original object also changed. This is how the spread operator works with nested objects.
The spread operator performs a deep copy only for top-level (or first-level) primitive values. In the original object, firstname
is a primitive value, so it’s deep copied. However, moreDetails
is a non-primitive value (an object), so it’s shallow copied. So, how can we deep copy nested data?
const original = {
firstname: 'Kannan',
moreDetails: {
lastname: 'Kannan'
}
};
const copied = {
...original,
moreDetails: { ...original.moreDetails }
} // spread operator
copied.moreDetails.lastname = "Ravindran"
console.log(original.moreDetails.lastname); // "Kannan"
console.log(copied.moreDetails.lastname); // "Ravindran"
As you see in the above example, we spread the moreDetails
object inside the original
object to ensure a deep copy of the nested data.
Object.assign():
Object.assign()
essentially performs the same function as the spread operator and was commonly used before the spread operator was introduced.
const original = {
firstname : 'Kannan',
moreDetails: {
lastname: 'Ravindran',
}
}
const addThis = { gender : 'Male' }
const copied = Object.assign({}, original);
const copied1 = Object.assign(addThis, original);
copied.firstname = 'Kannan Edited';
copied.moreDetails.lastname = 'Ravindran Edited';
console.log(original.firstname) // Kannan
console.log(original.moreDetails.lastname); // Ravindran Edited
console.log(copied.firstname); // Kannan Edited
console.log(copied1.gender); // Male
In the above example, we created two copies of the original
object, named copied
and copied1
.
When we modify the firstname
property in the copied
object, it doesn’t affect the original object. However, when we modify the moreDetails
object inside the copied
object, it also affects the original object, just like with the spread operator.
One thing to be careful about is that the first argument in Object.assign()
gets modified and returned as the copied object. To avoid unintentional side effects, you typically pass an empty object {}
as the first argument. In our example, we also demonstrated what happens when you pass a non-empty object as the first argument.
In the copied1
object, we passed addThis
as the first argument. As a result, the gender
property from addThis
is included, and its value (Male
) is logged to the console.
Deep Copy:
You can use the spread operator to deep copy an object or array if you know its structure. But what if you don’t know the structure of the data?
There are ways to deep copy non-primitive values. Let’s explore two of them:
JSON.parse(JSON.stringify()):
This method converts the object to a JSON string and then parses the string back into a new object.
const original = {
firstname : 'Kannan',
moreDetails : {
lastname: 'Ravindran'
}
}
const copied = JSON.parse(JSON.stringify(original));
copied.moreDetails.lastname = 'Ravindran - Edited';
console.log(original.moreDetails.lastname); // Ravindran;
console.log(copied.moreDetails.lastname); // Ravindran - Edited
As we can see, theoriginal
objects don’t change. However, this method has some limitations. It doesn’t handle functions, dates, or regular expressions.
structuredClone():
This is a modern method for deep copying objects. It also works with arrays and handles more data types than JSON.parse(JSON.stringify())
.
const original = {
firstname : 'Kannan',
moreDetails : {
lastname: 'Ravindran'
}
}
const copied = structuredClone(original);
copied.moreDetails.lastname = 'Ravindran - Edited';
console.log(original.moreDetails.lastname); // Ravindran;
console.log(copied.moreDetails.lastname); // Ravindran - Edited
While shallow copying works for simple, non-nested data, deep copying is necessary for complex structures.
Whether you use JSON.parse(JSON.stringify())
, structuredClone()
, or other methods, choosing the right approach depends on your specific use case.
Keep these tools in mind, and you’ll avoid unexpected bugs while writing cleaner, more predictable code.
Subscribe to my newsletter
Read articles from Kannan Ravindran directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Kannan Ravindran
Kannan Ravindran
I am a front-end developer, who loves writing and sharing knowledge with others...