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.

0
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...