Shallow Vs Deep Copy!

Diving deep to find out the best way of copying 😑

·

7 min read

Shallow Vs Deep Copy!

Every developer would have come across Copying knowingly or unknowingly (That's what we do right!) . But what is a copy?

In Layman's terms, Copy looks exactly like the original but the only difference is that when a copy is modified, the original does not get affected.

In Programming terms, We do a copy by assigning the same data to a new variable.

But, what about Shallow copy and Deep copy? What do these terms mean? How are they different from one another and what's the best way to make a copy? Let's find the answers to these questions!

Before that, we need to know what Primitive and Non-Primitive data-types are.

Primitive data types

  • Number
  • String
  • Undefined
  • Null
  • Boolean

These data types are the most basic ones. They do not contain any methods and are considered the lowest level of implementation in a language.

The advantage of these data types is that they exist only once. So, they do not create problems of Shallow & Deep copy.

Let's see a simple example.

let variableOne = 2000; // 2000

console.log(variableOne); // 2000

let variableTwo = variableOne; // copy

variableTwo = 2001;


console.log(variableTwo); //2001

console.log(variableOne); // 2000

Non-Primitive data types

  • Objects
  • Arrays etc.

The biggest problem with these data types is that these are stored only once during instantiation and then when a new variable is assigned to them, it just points to the same reference in the memory.

deep vs shallow.jpg

Since Arrays are Objects in JavaScript, most of what applies to Objects, also applies to Arrays. So, we'll only be looking at Objects. for now!

Let's see a simple example.


const playerOne = {
    username: "Rahul Dravid",
    place: "Indiranagar",
    title: "The Wall",
};

console.log(playerOne.title); // The Wall

const playerTwo = playerOne;

playerTwo.title = "Indiranagar ka Gunda";


console.log(playerTwo.title); // Indiranagar ka Gunda

console.log(playerOne.title); // Indiranagar ka Gunda

This is what a Shallow Copy is. In simplest terms, when modifying the copy, if the original get affected then it's a shallow copy of the original

This is usually considered a bad practice because, now if you try to access the title of playerOne it would give you a different result than what you expected.

So, how to make a proper copy i.e. Deep Copy? Let's have a look at the different ways we can.

1. Using Spread Operator ( . . . )

Let's take the following object.


const playerOne = {
    username: "Rahul Dravid",
    title: "The Wall",
    logger: function () {
        console.log(this.username);
    },
    matches: {
        test: 200,
        ODI: 400,
    },
    x:undefined,
    reg: /.*/, 
};

Now, let us create a new object playerTwo from playerOne using Spread and then modify the username property on playerTwo


const playerTwo = { ...playerOne }; // copy of playerOne

playerTwo.username = "Modified Dravid"; 

console.log(playerTwo.username); // Modified Dravid

console.log(playerOne.username) // Rahul Dravid

Great! We got what we wanted, now lets modify the matches.ODI count in playerTwo to 2.

console.log(playerOne.matches.ODI); // 400

playerTwo.matches.ODI = 2;

console.log(playerTwo.matches.ODI); // 2

console.log(playerOne.matches.ODI); // 2

What just happened? We thought that Spread creates a deep copy. Then how did changing the matches.ODI count in playerTwo affect playerOne?

This is because Spread does only one level of Deep copy i.e. the nested objects are not. Which means that matches in playerTwo and playerOne points to the same reference.

The good thing about Spread is that undefined , regex, methods are copied as it is.

2 . Using Object.assign({ })


console.log(playerOne.title); // The Wall

console.log(playerOne.matches.ODI); // 400

const playerTwo = Object.assign({}, playerOne);

playerTwo.title = "The Gentleman";

playerTwo.matches.ODI = 2000;


console.log(playerTwo.title); // The Gentleman

console.log(playerOne.title); // The Wall


console.log(playerTwo.matches.ODI); // 2000

console.log(playerOne.matches.ODI); // 2000

Object assign({}, toBeCopied) is doing the same thing as Spread. It makes a deep copy but only uptop one level.

The nested values are not deep copied, instead they share the same reference.

3. Using For...in

Yes, you've read it right! We'll make a copy using for...in loop


const playerOne = {
    username: "Rahul Dravid",
    title: "The Wall",
    logger: function () {
        console.log(this.username);
    },
    matches: {
        test: 200,
        ODI: 400,
    },
    x: undefined,
    re: /.*/,
};

let playerTwo = {};

for (let key in playerOne) {
    playerTwo[key] = playerOne[key];
}

console.log(playerTwo); // copy of playerOne

Now again, let's modify the data in playerTwo


console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 200


playerTwo.title = "Mr.Dependable";
playerTwo.matches.test = 5000;

console.log(playerTwo.title); // Mr.Dependable
console.log(playerTwo.matches.test); // 5000

console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 5000

This again produces the same result! One level of deep copy, any nested values again, point to the same reference.

4. JSON Parse & Stringify

This way of making a copy has an Ace up it's sleeve and a major flaw, let's see what they are!


const playerOne = {
    username: "Rahul Dravid",
    title: "The Wall",
    logger: function () {
        console.log(this.username);
    },
    matches: {
        test: 200,
        ODI: 400,
    },
    x: undefined,
    re: /.*/,
};

const playerTwo = JSON.parse(JSON.stringify(playerOne)); // copy of playerOne

Now, again, let's make some minor modifications on playerTwo

console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 200

playerTwo.title = "Jammy";

playerTwo.matches.test = 9;


console.log(playerTwo.title); // Jammy
console.log(playerTwo.matches.test); // 9

console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 200

😮🤯 Modifying playerTwo title as usual did not affect playerOne but so didn't the matches count.

So, JSON.parse(JSON.stringify(object)) actually produces a fully deep copy i.e. even the nested values create their own reference instead of sharing one.

This is great right? Then what was the problem I was talking about?

console.log(playerTwo.re) // {}
console.log(playerTwo.logger) // undefined

Here, playerTwo re property should have logged the regex and playerTwo logger property should have logged the function itself but instead we got {} and undefined

JSON.parse(JSON.stringify(object)) looses all these properties like , custom class instances, undefined, Infinity, functions, regex, maps, sets, or other complex types within it.

This can only be used when your object has native JavaScript values such as Number, String, Boolean etc i.e. when the original is JSON safe.

5. structuredClone()

structuredClone() is a webAPI method to make a deep copy.


const playerOne = {
    username: "Rahul Dravid",
    title: "The Wall",
    matches: {
        test: 200,
        ODI: 400,
    },
    x: undefined,
    re: /.*/,
};

const playerTwo =structuredClone(playerOne); // copy of playerOne

console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 200

playerTwo.title = "Jammy";

playerTwo.matches.test = 9;


console.log(playerTwo.title); // Jammy
console.log(playerTwo.matches.test); // 9

console.log(playerOne.title); // The Wall 
console.log(playerOne.matches.test); // 200

structuredClone() actually makes a copy without any loss in (period). Then why aren't we using it to make copies.

Well, as usual, this also has a catch, this looses the functions. The even bigger problem is that if you object has a function and you try to do a clone using this , it'll throw an error 🥲

const playerOne = {
    username: "Rahul Dravid",
    title: "The Wall",
    logger: function () {
        console.log(this.username);
    },
    matches: {
        test: 200,
        ODI: 400,
    },
    x: undefined,
    re: /.*/,
};

const playerTwo =structuredClone(playerOne)

// Uncaught DOMException: Failed to execute 'structuredClone' on 'Window': function ()

6. Lodash deepClone

The best way of making a pure deep copy is by relying on an external library. Yeah. Sad but true.

But using it is pretty simple, as follows.

import cloneDeep from "lodash.clonedeep";

const playerOne = {
  username: "Rahul Dravid",
  title: "The Wall",
  logger: function () {
    console.log(this.username);
  },
  matches: {
    test: 200,
    ODI: 400
  },
  x: undefined,
  re: /.*/
};


const playerTwo = cloneDeep(playerOne);

console.log(playerTwo.title); // Jammy
console.log(playerTwo.matches.test); // 9
console.log(playerTwo.re); // /.*/

console.log(playerOne) // no effects
console.log(playerTwo) // prints exact copy with zero problems

Conclusion

So, what's the conclusion? It's pretty simple.

To make a deep copy you can use

  1. structuredClone API provided that the object has no functions.

  2. JSON.parse(JSON.stringify(object)) provided that the object is JSON proof.

  3. Lodash cloneDeep is still the best bet to make a perfect clone.

spiderman.jpg

That's it for now! Please drop your thoughts in the comments. Happy coding.

https://twitter.com/Nikhil_Belide

References

Â