Deep vs Shallow Copy In JavaScript

Deep vs Shallow Copy In JavaScript

Introduction

Welcome JS Developers!! If you are here then there might be a chance where at least once in your life, you've been a victim of being tricked by JavaScript while making a copy of an Object and updating it. Well, trust me we've all been there, so today I've decided to make it very simple and provide some ways of how we can create a perfect Deep Copy of an object. LET'S BEGIN!

What is Deep and Shallow Copy in JavaScript

First, let's have a quick understanding of what actually is Deep and Shallow Copy in JavaScript.

A shallow copy of an object is a copy whose properties share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you may also cause the other object to change too — and so, you may end up unintentionally causing changes to the source or copy that you don't expect.

It means that whenever we create a copy of an object by directly assigning it to another variable, we are not creating a new object with the same properties, instead, we are just creating another reference that will point to the same object. So, we end up making 2 references to the same object. For e.g. ->

let carObject = {company: "Volkswagen", model : "Polo"}
let copiedCarObject = carObject;
console.log(copiedCarObject) // {company: "Volkswagen", model : "Polo"}
console.log(carObject)       // {company: "Volkswagen", model : "Polo"}

But what is the issue with this? Let's see.

copiedCarObject.model = "Jetta";
console.log(copiedCarObject) // {company: "Volkswagen", model : "Jetta"}
console.log(carObject)       // {company: "Volkswagen", model : "Jetta"}

What just happened? We tried to update the model for the copied object but ended up updating both of them. This happened because both the variables are references(pointing) to the same object. Hence, updating one will result in updating both of them. This type of copy is known as Shallow Copy. It can create a lot of confusion and errors in the code.

What is the solution to this? This is where Deep Copy comes into the picture.

A deep copy of an object is a copy whose properties do not share the same references (point to the same underlying values) as those of the source object from which the copy was made. As a result, when you change either the source or the copy, you can be assured you're not causing the other object to change too; that is, you won't unintentionally be causing changes to the source or copy that you don't expect.

It means that we create a copy in such a way that instead of creating a reference to the same object, we actually create a new object with the same properties as the first object. So, updating the copied object won't affect the first object.

What are the ways to create a Deep Copy of an Object

1. Using Object.assign()

Using Object.assign() we can copy properties of the source object into the target object.

Syntax

const returnedTarget = Object.assign(target, source);

let carObject = {company: "Volkswagen", model : "Polo"}
let copiedCarObject = Object.assign({},carObject);
console.log(copiedCarObject) // {company: "Volkswagen", model : "Polo"}
console.log(carObject)       // {company: "Volkswagen", model : "Polo"}

copiedCarObject.model = "Jetta";
console.log(copiedCarObject) // {company: "Volkswagen", model : "Jetta"}
console.log(carObject)       // {company: "Volkswagen", model : "Polo"}

2. Using {...} Spread Operator

Using the ES6 syntax, With the help of the spread operator, we can copy the properties of the source object into the target object.

Syntax

const returnedObject = {...sourceObject}

let carObject = {company: "Volkswagen", model : "Polo"}
let copiedCarObject = {...carObject}
console.log(copiedCarObject) // {company: "Volkswagen", model : "Polo"}
console.log(carObject)       // {company: "Volkswagen", model : "Polo"}

copiedCarObject.model = "Jetta";
console.log(copiedCarObject) // {company: "Volkswagen", model : "Jetta"}
console.log(carObject)       // {company: "Volkswagen", model : "Polo"}

We can see that using the above two methods, the original object is not affected while mutating the copied object. So is this good enough to be our solution? I don't think so.

let carObject = {
  model: "Polo",
  dateIssued: { month: "May", year: 2022 },
};
let copiedCarObject1 = Object.assign({}, carObject);
let copiedCarObject2 = { ...carObject };

console.log(copiedCarObject1); // { model : "Polo", dateIssued: { month: "May",year:2022 } }
console.log(copiedCarObject2); // { model : "Polo", dateIssued: { month: "May",year:2022 } }
console.log(carObject); // { model : "Polo", dateIssued: { month: "May",year:2022 } }

/* Let's try to modify the dateIssued */

copiedCarObject1.dateIssued.month = "June";

console.log(copiedCarObject1); // { model : "Polo", dateIssued: { month: "June",year:2022 } }
console.log(carObject); // { model : "Polo", dateIssued: { month: "June",year:2022 } }

copiedCarObject2.dateIssued.month = "July";

console.log(copiedCarObject1); // { model : "Polo", dateIssued: { month: "July",year:2022 } }
console.log(carObject); // { model : "Polo", dateIssued: { month: "July",year:2022 } }

In the above example, we saw that, as soon as we tried to clone a nested object, both the deep clone methods failed to create a deep copy and mutating the new object resulted in mutating the original object

3. Using JSON.parse() and JSON.stringify()

Using the JSON.stringify() first we can convert our object to string and the using JSON.parse(), we can convert it back to a new object.

Syntax

const returnedObject = JSON.parse(JSON.stringify(sourceObject));

let carObject = {
  model: "Polo",
  dateIssued: { month: "May", year: 2022 },
};
let copiedCarObject = JSON.parse(JSON.stringify(carObject));

console.log(copiedCarObject); // { model : "Polo", dateIssued: { month: "May",year:2022 } }
console.log(carObject); // { model : "Polo", dateIssued: { month: "May",year:2022 } }

copiedCarObject.dateIssued.month = "June";

console.log(copiedCarObject); // { model : "Polo", dateIssued: { month: "June",year:2022 } }
console.log(carObject); // { model : "Polo", dateIssued: { month: "May",year:2022 } }

We can see that this method has solved our problem, but again this is also not a full-proof solution. Why Not? Let's check

let carObject = {
  model: "Polo",
  getModelName: () => {
    return this.model;
  },
  createDate: new Date(),
};
let copiedCarObject = JSON.parse(JSON.stringify(carObject));

console.log(copiedCarObject);
// { model: 'Polo', createDate: '2022-07-22T14:18:38.229Z' }
console.log(carObject);
// { model: 'Polo', getModelName: [λ: getModelName], createDate: Fri Jul 22 2022 19:49:46 GMT+0530 (India Standard Time) }

console.log(typeof copiedCarObject.createDate) //string
console.log(typeof carObject.createDate) //object

The First Major Issue that can be seen in the above example is that our function getModelName is missing in the copied object. Secondly, the type of createDate got changed from object to string. Then is there any way at all to create a true deep clone of an object? Well lucky for us, there is one.

4. Using external library loadash

loadash provides a cloneDeep function which can be used to create true Deep clones of objects.

Install using npm i --save lodash

const _ = require("lodash");

let carObject = {
  model: "Polo",
  getModelName: () => {
    return this.model;
  },
  createDate: new Date(),
};

let copiedCarObject = _.cloneDeep(carObject);

console.log(copiedCarObject);
// { model: 'Polo', getModelName: [λ: getModelName], createDate: Fri Jul 22 2022 19:52:46 GMT+0530 (India Standard Time) }
// { model: 'Polo', getModelName: [λ: getModelName], createDate: Fri Jul 22 2022 19:52:46 GMT+0530 (India Standard Time) }

console.log(typeof copiedCarObject.createDate) //object
console.log(typeof carObject.createDate) //object

Here we can see the function is also present and the type of date is also retained.So, this is how you can create a true Deep Copy of an Object.

However, not everyone is a fan of relying on libraries. So let's check our own implementation of deep cloning an object.

5. Creating Our Own implementation of cloneDeep using recursion

Here we are iterating the keys of source object, and checking the typeof value, if it's an object, then recursively calling the function else saving it as it is in the target.

let carObject = {
  model: "Polo",
  getModelName: () => {
    return this.model;
  },
  createDate: new Date(),
  date: { year: "2022" },
};

function deepClone(source) {
  let target = Array.isArray(source) ? [] : {};
  const keys = Object.keys(source);
  if (keys.length > 0) {
    keys.forEach((key) => {
      if (typeof source[key] === "object") {
        target[key] = deepClone(source[key]);
      } else {
        target[key] = source[key];
      }
    });
  } else {
    target = source;
  }
  return target;
}

const copiedCarObject = deepClone(carObject);

console.log(copiedCarObject);
// { model: 'Polo', getModelName: [λ: getModelName], createDate: Fri Jul 22 2022 19:59:13 GMT+0530 (India Standard Time) }
console.log(carObject);
// { model: 'Polo', getModelName: [λ: getModelName], createDate: Fri Jul 22 2022 19:59:13 GMT+0530 (India Standard Time) }

console.log(typeof copiedCarObject.createDate); //object
console.log(typeof carObject.createDate); //object

Conclusion

I hope after reading this, JavaScript won't be tricking us anymore while creating a copy of an object.

Do share if you find it useful.

You can connect with me on LinkedIn and Twitter.