Cloning an object in JavaScript and avoiding Gotchas

If you're a JavaScript developer, you must have come across scenarios where you need to clone an object. How do you do it? In this article we will cover various approaches to clone an object in JavaScript and their shortcomings and finally talk about the most reliable way to make a deep copy (clone) of an object in JavaScript.

Let us consider that our object to be cloned is this:

const person = {
  name: 'Dolores Abernathy',
  age: 32,
  dob: new Date('1988-09-01')
}

There can be various ways to clone it:

One way would be to declare a new variable and point it to the original object (which is not exactly cloning the object)

const clone = person

What you're doing here is you are referencing the same object. If you change clone.name, person.name will also change. Most of the times, this is not what you intend to do when you want to clone an object. You would want a copy of the object which does not share anything with the original object. Here, clone is just a reference to the same object being referred by person. Most of the JavaScript developers would know about this. So, this is not really a "Gotcha!". But the next two approaches I am going to show are definitely something you need to watch out for.

You'll often see code using spread operator to clone an object. For example:

const clone = { ...person }

Or code using Object.assign like this

const clone = Object.assign({}, person)

One might assume in both of the above cases that clone is a copy of the original person object and does not share anything with the original object. This is partially correct but can you guess the output of the code below? (Please take a moment to think what the output should be before copy pasting it)

const person = {
  name: 'Dolores Abernathy',
  age: 32,
  dob: new Date('1988-09-01')
}

const clone = { ...person }

// change the year for person.dob
person.dob.setYear(1986)

// check the clone's dob year
console.log(clone.dob.getFullYear())

What was your guess? 1988?

The correct answer is 1986. If you guessed the right answer and know the reason behind it, good! You have strong JavaScript fundamentals. But if you guessed it wrong, that's ok. It's the reason why I am sharing this blog post because a lot of us assume that by using the spread operator, we are creating a completely separate copy of the object. But this is not true. Same thing would happen with Object.assign({}, person) as well.

Both these approaches create a shallow copy of the original object. What does that mean? It means that all the fields of the original object which are primitive data types, will be copied by value but the object data types will be copied by reference.

In our original object, name and age are both primitive data types. So, changing person.name or person.age does not affect those fields in the clone object. However, dob is a date field which is not a primitive datatype. Hence, it is passed by reference. And when we change anything in dob field of the person object, we also modify the same in clone object.

How to create a deep copy of an object ?

Now that we know that both the spread operator and the Object.assign method create shallow copies of an object, how do we create a deep copy. When I say deep copy, I mean that the cloned object should be a completely independent copy of the original object and changing anything in one of those should not change anything in the other one.

Some people try JSON.parse and JSON.stringify combination for this. For example:

const person = {
  name: 'Dolores Abernathy',
  age: 32,
  dob: new Date('1988-09-01')
}

const clone = JSON.parse(JSON.stringify(person))

While it's not a bad approach, it has its shortcomings and you need to understand where to avoid using this approach.

In our example, dob is a date field. When we do JSON.stringify, it is converted to date string. And then when we do JSON.parse, the dob field remains a string and is not converted back to a date object. So, while clone is a completely independent copy of the person in this case, it is not an exact copy because the data type of dob field is different in both the objects.

You can try yourself

console.log(person.dob.constructor) // [Function: Date]
console.log(clone.dob.constructor) // [Function: String]

This approach also doesn't work if any of the fields in the original object is a function. For example

const person = {
  name: 'Dolores Abernathy',
  age: 32,
  dob: new Date('1988-09-01'),
  getFirstName: function() {
    console.log(this.name.split(' ')[0])
  }
}

const clone = JSON.parse(JSON.stringify(person))

console.log(Object.keys(person)) // [ 'name', 'age', 'dob', 'getFirstName' ]

console.log(Object.keys(clone)) // [ 'name', 'age', 'dob' ]

Notice that the getFirstName is missing in the clone object because it was skipped in the JSON.stringify operation as it is a function.

What is a reliable way to make a deep copy/clone of an object then?

Up until now all the approaches we have discussed have had some shortcomings. Now we will talk about the aproach that doesn't. If you need to make a truly deep clone of an object in JavaScript, use a third party library like lodash

const _ = require('lodash')

const person = {
  name: 'Dolores Abernathy',
  age: 32,
  dob: new Date('1988-09-01'),
  getFirstName: function() {
    console.log(this.name.split(' ')[0])
  }
}

const clone = _.cloneDeep(person)

// change the year for person.dob
person.dob.setYear(1986)

// check clone's dob year
console.log(clone.dob.getFullYear() // should be 1988

// Check that all fields (including function getFirstName) are copied to new object
console.log(Object.keys(clone)) // [ 'name', 'age', 'dob', 'getFirstName' ]

// check the data type of dob field in clone
console.log(clone.dob.constructor) // [Function: Date]

You can see that the cloneDeep function of lodash library will make a truly deep copy of an object.

Conclusion

Now that you know different ways of copying an object in JavaScript and pros and cons of each approach, I hope that this will help you in making a more informed decision on which approach to use for your use case and avoid any "Gotchas" while writing code.

Happy Coding :-)