Ahoy there! Let's meet the biggest Object Trap in JavaScript!!
Did you know that you can not copy an object? I mean you can't use any of these following methods to purely copy an object in JavaScript.
If you use these methods, you'll get really unexpected results. Like, if you change y, it'll also change x. (Same for arrays too)
These methods will NOT work
let x = { name: 'SilvenLEAF', age: 19 }
// Method 1
let y = x
// Method 2
let y = {...x}
// Method 3
let y = Object.assign(x)
Did you know why it happens? And also how to solve it?
If not, let's dive in depth why and how JavaScript does that.
Data Types in JavaScript
There are 2 types of data in JavaScript.
Primitive Data Types: e.g. Number, String, Boolean, null, undefined
Referencial Data Types: e.g. Objects, Arrays
Main Concept
When we store a primitive value in a variable, we are storing the value in that variable. But, when we are storing a referencial value in a variable, we are storing its reference in that variable.
let x = SOME_VALUE;
let y = x;
If x is a primitive data type then, y will be a pure copy of x where x and y are not linked in any way. I mean, if you change the value of y, it will not affect the value of x
let x = 5
let y = x
console.log(y) // 5;
y = 7
console.log(y) // 7
console.log(x) // 5
But if x is a referencial value, then y and x will be two variables for the same value. So if you change y, it'll also change x. Because they are just two names for the same object.
let x = { name: 'SilvenLEAF', age: 19, isFemale: false } // here x is the reference for this object
let y = x // now both x and y are referencing that same object, so you can say, two names for the same object
console.log(y) // { name: 'SilvenLEAF', age: 19, isFemale: false }
y.name = 'Manash'
console.log(y) // { name: 'Manash', age: 19, isFemale: false }
console.log(x) //{ name: 'Manash', age: 19, isFemale: false }
Same thing is applicable for Arrays as well:
let x = [1,2,3,4,5]
let y = x
console.log(y) // [1,2,3,4,5]
y[0] = 'a'
console.log(y) // ['a',2,3,4,5]
console.log(x) // ['a',2,3,4,5]
Analogy:
Now let's understand this concept with simplest analogies.
Analogy for Primitive Data Type:
let x = 'water'
// In the x bucket we have 5 litre water.
let y = x
// Hey computer, in the y bucket, store the same kind of thing that we have on x
// The computer stores 5 litre water in the y bucket as well
y = 'apples'
// Hey computer, I changed my mind,
// Keep 5 apples in the y bucket
console.log(y)
// What do I have in my y bucket now?
// 5 apples
console.log(x)
// What do I have in my x bucket?
// 5 litre water
// Why?
// We changed the content of y bucket
// but we did not change the content of x bucket
// NOTE: x bucket and y backet had seperate 5 litre water.
// Not the same water in both buckets.
// Because I told the computer to store the same type and same amount of thing that we had on x bucket
Another analogy if you are still confused
// Another analogy is:
let x = 'chocolate'
// x girl buys a chocolate
y = x
// y girl tells her dad, "Daddy daddy, what is she (x girl) buying? I wanna have the same"
// Now her dad gives her that type of chocolate.
// Now both x and y have same type of chocolate
y = 'ice cream'
// y girl changes her mind, "Yuck, I don't like this flavour,
// I don't want it anymore, give me ice cream instead"
// Her dad now buys her an 'ice cream'
// Now y girl has an ice cream. What does x girl have?
// A chocolate. Because y girl changed her mind.
// It doesn't change the fact that x girl bought a chocolate
// Hope you get my point
Analogy for Referencial Data Type:
// "The_book_on_the_3rd_drawer" is this book
let The_book_on_the_3rd_drawer = {
title: 'A book of insanity',
author: 'SilvenLEAF',
rating: 9,
}
let the_weird_book = The_book_on_the_3rd_drawer
// the_weird_book is now referencing The_book_on_the_3rd_drawer
// Hey computer, name the book on my 3rd drawer as the_weird_book.
// So in future if I say, "Change the title of the_weird_book",
// you will change the title of that book (which is the book on my 3rd drawer).
let my_favorite_book = the_weird_book
// Hey, name the_weird_book as my_favorite_book.
// Hey, name the book on my 3rd drawer as my_favorite_book
// So now the book on my third drawer has two names, the_weird_book and my_favorite_book
// So if I say, where is the_weird_book?
// It is in your 3rd drawer my master
// Then where is my_favorite_book?
// It is in your 3rd drawer my master
// Why?
// Because they are the same book with 2 names
my_favorite_book.author = 'Manash'
// make the author of my_favorite_book as 'Manash'
console.log(my_favorite_book) // { title: 'A book of insanity', author: 'Manash', rating: 9 }
console.log(the_weird_book) // { title: 'A book of insanity', author: 'Manash', rating: 9 }
// Who is the author of my_favorite_book?
// Manash
// Who is the author of the_weird_book?
// Manash
// Why?
// Because you gave two names for the same book.
Another analogy if you are still confused
// here SilvenLEAF is this boy
let SilvenLEAF = {
name: 'Manash Sarma',
age: 19,
what_I_like_about_him: 'His projects'
}
let the_clumpsy_kid = SilvenLEAF
// Hey computer, let's call SilvenLEAF as the_clumpsy_kid
let the_typescript_addict = the_clumpsy_kid
// Hey computer, let's call that clumpsy kid as "the typescript addict"
// Hey computer, let's call SilvenLEAF as "the typescript addict"
the_typescript_addict.what_I_like_about_him = 'His blogs'
// Hey computer, update this info, what I like about the typescript addict is his projects
console.log(the_typescript_addict)
console.log(the_clumpsy_kid)
// Both has this value {
// name: 'Manash Sarma',
// age: 19,
// what_I_like_about_him: 'His blogs'
// }
// Hey what is the thing I like about the clumpsy kid?
// His blogs
// Hey what is the thing I like about the typescript addict?
// His blogs
// Why?
// They are the same boy.
// You gave two names for the same boy
More on Object Cloning
Deep Clone vs Shallow Clone
Since we are talking about cloning, you might encounter these two words at some point. What is a deep clone and a shallow clone?
When we clone a variable from another variable, if both of them are totally independant and not linked in anyway (including all their nested values), I mean if I change one then it does not change the other, this cloning is called Deep cloning.
You can change all nested values of y and it should not change the values of x
let x = {
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
let y = JSON.parse(JSON.stringify(x)); // I cloned x to y
console.log(y)
console.log(x)
/*
Both of them have these values now
{
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
*/
y.name = 'Manash'
y.more_info.favorite_language = 'Chinese'
console.log(y)
/*
{
name: 'Manash', age: 19,
more_info: {
favorite_language: 'Chinese',
total_languages: 7,
}
}
*/
console.log(x)
/*
{
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
*/
// You can change any value of y and it will not change x
But, if any nested object in that clone maintains their reference, I mean if you change that nested object and it also changes the nested object from the original parent, then this cloning is called Shallow cloning,
let x = {
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
// method 1
let y = {...x}
// method 2
let y = Object.assign({}, x)
y.name = 'Manash';
y.more_info.favorite_language = 'Chinese';
console.log(y)
/*
{
name: 'Manash', age: 19,
more_info: {
favorite_language: 'Chinese',
total_languages: 7,
}
}
*/
console.log(x)
/*
{
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'Chinese',
total_languages: 7,
}
}
// When I changed y.name it did not change x.name
// but when I changed y.more_info, it also changed x.more_info as well
// because x.more_info is a referencial value and when we cloned x into y with the above methods
// it cloned and created a new object but y.more_info is still maintaining the reference of x.more_info
// these two are still pointing to the same object
Conquering Object Trap
Well we saw that we can't deep clone the object with these following methods
let x = {
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
// Method 1 (Assigning reference for nested objects)
let y = x;
/*
This is NOT a shallow cloning
but assigning the existing object to y by reference.
Thanks goes to @jamesthomson for pointing that out
*/
// Method 2 (Shallow cloning)
let y = {...x};
/*
it will work if x has only primitive values,
but if x has a referencial value inside it
(I mean, an object or an array inside x)
then it will not work for that referencial value.
We'll discuss about it in the advanced section below
*/
// Method 3 (Shallow cloning)
let y = Object.assign({}, x);
// This one is same as Method 2
Then how do we make a deep clone? (Deep clone means having same value but totally independant and not linked in any way, so that if we change one, the other one will not get changed)
It's super EASY!!
let x = {
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
let y = JSON.parse(JSON.stringify(x)); // we cloned x to y
Why does it work? "JSON.stringify()" turns x into a primitive value, and as we know, if it is a primitive value, it'll make a pure deep clone. Now we are converting the pure deep clone (the JSON string) into an object. And this object is purely independant and not linked to x in any way
So now if you change anything from y, it will not change anything from x
let x = {
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
let y = JSON.parse(JSON.stringify(x)); // we cloned x to y
console.log(y)
console.log(x)
/*
Both of them have these values now
{
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
*/
y.name = 'Manash'
y.more_info.favorite_language = 'Chinese'
console.log(y)
/*
{
name: 'Manash', age: 19,
more_info: {
favorite_language: 'Chinese',
total_languages: 7,
}
}
*/
console.log(x)
/*
{
name: 'SilvenLEAF', age: 19,
more_info: {
favorite_language: 'TypeScript',
total_languages: 7,
}
}
*/
// You can change any value of y and it will not change x
Yea, I know it's a bit clumsy method. But that's what I could find. Please share in the comments if you know any better solution.
Thanks to @jamesthomson
"JSON.stringify is one method and is acceptable in some use cases, however it's not a foolproof method as it will destroy nested functions within the object." --- James Thomson
"To truely perform a deep clone, you have to loop through the objects contents, see Lodash for an example on this." --- James Thomson
Advanced Section
Why does "let y = {...x}" not work?
let objectA = {
name: 'SilvenLEAF',
age: 19,
blogs: {
source: 'Dev.to',
themeColor: 'red',
}
}
// here objectA has "name" and "age" two primitive values and "blogs" referencial value
let objectB = { ...objectA };
console.log(objectB)
console.log(objectA) // both of their content is same as objectA
objectB.name = 'Manash'
objectB.blogs.source = 'Hashnode'
console.log(objectB)
/*
{
name: 'Manash',
age: 19,
blogs: {
source: 'Hashnode',
themeColor: 'red',
}
}
*/
console.log(objectA)
/*
{
name: 'SilvenLEAF',
age: 19,
blogs: {
source: 'Hashnode',
themeColor: 'red',
}
}
*/
Look, the primitive values of objectB are independant and not linked to those of objectA. But, for the referencial value of objectB is still linked to the referencial value of objectA.
So when we changed "objectB.name" it did not change "objectA.name". But when we changed "objectB.blogs" it also changed "objectA.blogs" because they both are the reference of the same object.
Now still confused. Don't worry, let's see what the spread operator actually is
// "let y = {...x}" actually means this
let y = {
name: x.name, // deep clone (because name is primitive)
age: x.age, // deep clone (because name is primitive)
blogs: x.blogs, // shallow clone (because name is referencial)
}
Or in other words,
// "let y = {...x}" actually means this
let y = {};
y.name = x.name // deep clone (because name is primitive)
y.age = x.age // deep clone (because name is primitive)
y.blogs = x.blogs // shallow clone (because name is referencial)
Now that makes sense right?
Why does "let y = Object.assign(x)" not work?
Same as "let y = {...x}" explained above
Congratulation if you made it this far. Hopefully I was able to clarify it. Let me know if you are still confused.
What's NEXT?
1. Learning DevOps with Github Actions
2. More on DevOps
3. Improved AI BOT that can do anything
4. Insane stuff with JavaScript/TypeScript
5. Debugging TypeScript with VS Code Debugger
6. Sequelize Hooks
7. How to create an Android APP with NO XP
(including apk generating)
Got any doubt?
Drop a comment or Feel free to reach out to me @SilveLEAF on Twitter or Linkedin
Wanna know more about me? Come here!
SilvenLEAF.github.io
Top comments (3)
There's a few things here that are a bit misleading...
In your "shallow clone" example, you aren't really cloning anything. You are simply creating a new variable and assigning an existing object to that variable by reference. So when you say
You haven't actually performed a shallow clone, just assigned an existing reference to a new variable.
A shallow clone is when you actually create a new object based on another, but any nested objects within that clone maintain their reference. So, your example
let objectB = { ...objectA };
is considered a shallow clone.A deep clone will clone the entire contents of an object so that it is truely a unique clone and therefore maintains no references from the source object. Your example with
JSON.stringify
is one method and is acceptable in some use cases, however it's not a foolproof method as it will destroy nested functions within the object. To truely perform a deep clone, you have to loop through the objects contents, see Lodash for an example on this.Oh, thank you sooo much Sir @jamesthomson for pointing this out. Really appreciate it. Updated the blog
👌