Deep vs Shallow copy

What exactly is deep or shallow copy?

According to the MDN docs,

Deep copy

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.

Shallow Copy

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.

Now this sounds good and all but first, lets talk about the data types in Javascript. Primarily JS consists of two different types of data types - primitive and composite.

Primitive data types

Primitive data types can be anything like String, Number, Big Int, Boolean etc. Most of the time, a primitive value is represented directly at the lowest level of the language implementation. Now, these are quite simple. Each copy is going to be it's own. So, when you write:

let city = "Boston";

//copy city to newCity
let newCity = city;

console.log("city:", city);
console.log("newCity:", newCity);

//reassign newCity value
newCity = "Tallahasee";
console.log("city:", city);
console.log("newCity after change:", newCity);

You'll get the expected output which would be:

1.PNG

Right then, now things are a little different when we talk about something like an array or an object.

Lets see it in action, shall we?

const stateObject = {
  state: "Michigan"
};

const newStateObject = stateObject;
newStateObject.state = "Texas";

console.log(stateObject);
console.log(newStateObject);

When we check the output, we'd see something like this:

2.PNG

Now that's weird we only changed the newStateObject but the initial state, which is the stateObject also changes.
Now lets see why this happens.

Composite Data Types

Now the example above ran into weird issues when we started using objects. You see that is because objects and arrays are Composite data types. So when we were assigning "newStateObject" to "stateObject" we were just creating a shallow copy of the "stateObject", which is why the change to "newStateObject.state" also changed the "stateObject". The values get stored once on initiation and reassigning it just creates a reference to that value. So how do you copy objects? Well, there are a couple of ways.

1. Object.assign

The Object.assign() method copies all enumerable own properties from one or more source objects to a target object. It returns the modified target object.

So, using our previous example:

const stateObject = {
  state: "Michigan"
};

const newStateObject = Object.assign({}, stateObject);
newStateObject.state = "Texas";

console.log("inital state:", stateObject);
console.log("state after chnages", newStateObject);

If we check the console now: 3.PNG

Pretty neat, huh? Just keep in mind that the first argument of Object.assign() is the target where the data will be copied.

2. The Spread Operator (...)

The spread operator lets you just spread out all the values in any iterable like an array or an object. Now suppose we had multiple states in our object and wanted to change one of them. We'd do it like so:

const stateObject = {
  stateOne: "Michigan",
  stateTwo: "Texas",
  stateThree: "Wisconsin"
};

//Changing stateThree

const newStateObject = {...stateObject, stateThree: "Massachusetts"}
console.log(newStateObject)

This would print out the desired output.

4.PNG

This works fine for simple objects that don't have a complicated structure. However, if your object had a complicated structure like say the cities in each state, things would get hairy.

3. JSON.parse and JSON.stringify

You could argue why is this even necessary? Can't we just spread the whole object and then just change the nested value we want? Well, it doesn't work like that. See, even if you were to spread the object its nested values would not create a deep copy of the nested objects.

Lets assume our object got a bit complicated with some nested data in it. To copy it we could do something like this:

const stateObject = {
  stateOne: "Michigan",
  cities: {
    cityOne: "Detroit",
    cityTwo: "Grand Rapids",
    cityThree: "Warren"
  }
};

const newStateObject = {...stateObject};
newStateObject.cities.cityOne = "Sterling Heights";
console.log("stateObject", stateObject);
console.log("newStateObject", newStateObject);

However the output would be something like this:

5.PNG

But we only wanted to make a change in the newStateObject right? This is the problem we run into with nested objects. Sure you can use the spread operator at each level of the object but that would get cumbersome as the object gets bigger. There may even be a scenario where you might be unaware of the objects structure. To fix this you could do something like so:

const newStateObject = JSON.parse(JSON.stringify(stateObject));
newStateObject.cities.cityOne = "Sterling Heights";
console.log("stateObject", stateObject);
console.log("newStateObject", newStateObject);

This would result in:

6.PNG

Which is the desired output.

These are some of the ways you would copy objects. When it comes to arrays you could use the spread operator or make use of array specific functions like filter, map, reduce, concat, from etc.

Conclusion

  1. A deep copy of an object is a copy whose properties do not share the same references.
  2. A shallow copy of an object is a copy whose properties share the same references.
  3. Make use of Object.assign(), spread operator or JSON.parse() along with JSON.stringity to create deep copy of objects.
  4. You can make deep copy of arrays using filter, map, reduce etc.

Further reading/References

  1. Deep Copy MDN
  2. Shallow Copy MDN
  3. PrimitivesMDN
  4. FreeCodeCamp

Lets connect over at Twitter, LinkedIn