DEV Community

Cover image for JavaScript Object Trap Simplified by SilvenLEAF
SilvenLEAF
SilvenLEAF

Posted on • Updated on

JavaScript Object Trap Simplified by SilvenLEAF

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)
Enter fullscreen mode Exit fullscreen mode

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.

  1. Primitive Data Types: e.g. Number, String, Boolean, null, undefined

  2. 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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 }
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode


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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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',
  }
}
*/

Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
jamesthomson profile image
James Thomson

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

But, if I changed one and it changed the other, this cloning will be called Shallow cloning because they are cloned from the outside but are same from the inside. They both are the same object.

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.

Collapse
 
silvenleaf profile image
SilvenLEAF • Edited

Oh, thank you sooo much Sir @jamesthomson for pointing this out. Really appreciate it. Updated the blog

Collapse
 
arvndvv profile image
Aravind V V

👌