DEV Community

Cover image for The pitfalls πŸ•³οΈ of cloning objects in JS (with Object.assign & object spread)
Benjamin Mock
Benjamin Mock

Posted on • Originally published at codesnacks.net

The pitfalls πŸ•³οΈ of cloning objects in JS (with Object.assign & object spread)

So how do you clone or copy an object in JS? Right ... use Object.assign or the Object spread operator. Let's see both of them in action:

const dog = {Β name: "Doggo" }
const clonedDogAssign = Object.assign({}, dog)
const clonedDogSpread = { ...dog }

That works perfectly fine. You can check that it's actually not the same object anymore, but that it still has the same properties

console.log(dog === clonedDogAssign) // false
console.log(clonedDogAssign) // {name: "Doggo"}
console.log(dog === clonedDogSpread) // false
console.log(clonedDogSpread) // {name: "Doggo"}

Alright. Now let's change the name of our cloned dog and see what'll happen to the original dog.

clonedDogAssign.name = "Dolly"
// let's check the clone
console.log(clonedDogAssign) // {name: "Dolly"}
// and the original
console.log(dog) // {name: "Doggo"}

So nothing happens to the original when the clone is changed. Perfect! ... as long as you're not dealing with nested objects!

Let's check that!

const dog = { name: "Doggo", owner: { name: "Ben" } }
// clone the dog
const clonedDog = { ...dog }
// change the name
clonedDog.name = "Dolly"
// and change the name of the owner
clonedDog.owner.name = "Louis"

console.log(clonedDog) // {name: "Dolly", owner: {name: "Louis"}}
console.log(dog) // {name: "Doggo", owner: {name: "Louis"}}

What? So the owner of the original dog changed because we changed the owner of the clone? This is because the object spread operator, as well as Object.assign, does not clone the values of nested objects, but copies the reference to the nested object. That's called shallow copying.

So how can we deep clone / deep copy an object, so that also the values of nested objects are cloned?

How to deep clone / deep copy JS objects

The tricked I learned back in the days is JSON serialization. An object is converted to JSON and then parsed again to get a new object. That trick works only as long as you're just dealing with primitive types or objects. For everything else, it won't work! Use this with caution!

const dog = { name: "Doggo", owner: { name: "Ben" } }
const clonedJsonDog = JSON.parse(JSON.stringify(dog))
clonedJsonDog.name = "Dolly"
clonedJsonDog.owner.name = "Louis"
console.log(clonedJsonDog) // {name: "Dolly", owner: {name: "Louis"}}
console.log(dog) // {name: "Doggo", owner: {name: "Ben"}}

Perfect! The original is kept untouched! So that's a working solution. This solution is not enough if you're dealing with functions or symbols within your objects. There's just no other way than to keep the reference to these. The JSON trick can't handle these types. In such a case the lodash clonedeep method is probably the best way to go.

// npm i lodash
const clonedeep = require('lodash.clonedeep')

const dog = { name: "Doggo", owner: { name: "Ben" } }
const deepClone = clonedeep(dog)
deepClone.name = "Dolly"
deepClone.owner.name = "Louis"
console.log(deepClone) // {name: "Dolly", owner: {name: "Louis"}}
console.log(dog) // {name: "Doggo", owner: {name: "Ben"}}

What's your solution for deep cloning / copying? Drop me a line in the comments! I'm curious!


Want to get better at Web Development?
πŸš€πŸš€πŸš€subscribe to my weekly βœ‰οΈnewsletter

Top comments (2)

Collapse
 
patarapolw profile image
Pacharapol Withayasakpunt

yaml.load(yaml.dump(obj, options)), so that I can pass in options like no-refs.

I also use this to hash objects (options.sortKeys + SparkMD5)

Another options is to use lodash.cloneDeep

This will preserve special objects, such as RegExp or Date object.

BTW, if you don't need special objects, and want to preserve typing in TypeScript, you can

export function clone<T>(a: T): T {
  return JSON.parse(JSON.stringify(a));
}
Collapse
 
mikgross profile image
Mikael

You can also use a library called 'lodash'. It has a .cloneDeep() method that does exactly that.

import * as _ from {'lodash'};

const clone = _.cloneDeep(clonedObject)