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.
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
structuredClone API provided that the object has no functions.
JSON.parse(JSON.stringify(object)) provided that the object is JSON proof.
Lodash cloneDeep is still the best bet to make a perfect clone.
That's it for now! Please drop your thoughts in the comments. Happy coding.
https://twitter.com/Nikhil_Belide