loading...
Cover image for Copying objects in JavaScript

Copying objects in JavaScript

ip127001 profile image Rohit Kumawat Updated on ・3 min read
Scenario:

Whenever we pass objects between components as props or as arguments in function, we need to copy that object to make sure that it doesn't affect the original one. Now we can easily make any changes to the copied object to our need.

So, following are the methods that we can use to do so:

  • Spread operator
  • Object.assign
  • JSON.parse and JSON.stringify

Does using any of these methods entirely copy the object? Will it also copy a nested object?

Let's look at another example:

let deepObj = {a: 4: b: {name: 'react'}, d: {name: 'angular'}};

So deepObj is a nested object and when it comes to copying nested objects i.e. objects with values as references, there comes the concept of shallow copy and deep copy.

  • Shallow copy: Only copies one level meaning if any of the value is a reference type then copy the reference but the exact value is not copied in the new object.

  • Deep copy: Copy every level of nested values even if it is a reference type like our example with deepObj object above.

Let's go one by to try copying the objects:

1. Spread Operator & Object.assign():

Example 1:

let obj1 = {a: 3, b: 4, c: "react"}, copiedObj1 = {};

copiedObj1 = {...obj1};
copiedObj1.c = "angular";

console.log(copiedObj1, obj1); 
{a: 3, b: 4, c: "angular"}
{a: 3, b: 4, c: "react"}



let obj2 = {a: 3, b: 4, c: "react"}, copiedObj2 = {};

copiedObj2 = Object.assign({}, obj2);
copiedObj2.c = "vue";

console.log(copiedObj2, obj2); 
{a: 3, b: 4, c: "vue"}
{a: 3, b: 4, c: "react"}

Both will perfectly copy the object because there is no reference type in object values and if you try to change any property that will not have any effect on copied object.

Example2:

let obj1 = {a: 3, c: [1,2]}, newObj1 = {};
newObj1 = {...obj1};
newObj1.c.push(5);

console.log(newobj1, obj1); 
{a: 3, c: [1,2,5]} 
{a: 3, c: [1,2,5]}


let obj2 = {a: 3, c: [1,2]}, newObj2 = {};
newObj2 = Object.assign({}, obj2);
newObj2.c.push(5);

console.log(newobj2, obj2); 
{a: 3, c: [1,2,5]}
{a: 3, c: [1,2,5]}

Here property c value is changes to [1,2,5] in both objects, so this is not perfectly copied because of reference type i.e. array ([1,2]). It just copies the reference to the array. Hence Spread operator and Object.assign() only does shallow copying not deep copying.

2. JSON.parse() and JSON.stringify():

var obj1 = {a: 3, b: 4, c: "react"};
var copiedObj1 = JSON.parse(JSON.stringify(obj1));
copiedObj1.c = "node";

console.log(copiedObj1, obj1); 
{a: 3, b: 4, c: "node"}
{a: 3, b: 4, c: "react"}


var obj2 = {a: 3, c: [1,2]};
var copiedObj2 = JSON.parse(JSON.stringify(obj2));
copiedObj2.c.push(5);

console.log(copiedObj2 , obj2);
{a: 3, c: [1,2,5]} 
{a: 3, c: [1,2]}

This perfectly copies the object as any change in copied object in both case doesn't have any effect in original object.

But this works only in those cases where value is converted to string first and it gets parsed again.
Following are few of the cases where it fails to copy the object.

let obj = {
  name: 'laptop',
  value: function () {
    return 100000';
  }
}

let copiedObj = JSON.parse(JSON.stringify(obj));

console.log(copiedObj);
{name: 'laptop'}

Failed -> It removed the value method from copied object.

let obj = {a: undefined, b: new Date()}

let copiedObj = JSON.parse(JSON.stringify(obj));

console.log(copiedObj);
{b: "2020-06-06T16:23:43.910Z"}

Failed -> Removed the first property and converted the date value to string;


For Shallow copying use

  1. Spread operator
  2. Object.assign().

For Deep copying

  1. Use lodash library cloneDeep method (_.cloneDeep(any nested object))

  2. Make a custom function which will handle reference types like in below example covering just one case.

function deepCopy(obj) {
    let copiedObj = {};
    for(key in obj) {
        if(Array.isArray(obj[key])) {
            copiedObj[key] = [...obj[key]];
        } else {
            copiedObj[key] = obj[key]
        }
    }
    return copiedObj;
}

var obj = {value1: 5, value2: [1,2,3]};

var copiedObj = deepCopy(obj);
copiedObj.value2.push(5);

console.log(copiedObj , obj);
{value1: 5, value2: [1,2,3,5]}
{value1: 5, value2: [1,2,3]}

Better way will be to use recursion to handle reference type

So there are other libraries exists which provide good performance event to do deep cloning as you saw it need more cumputation or you can make a custom function and add more edge case.

Conclusion: Always watch for values if any is reference types in the object that is being passed around. And use shallow and deep copying better.

Thanks for reading!

Posted on by:

ip127001 profile

Rohit Kumawat

@ip127001

Helping startups to build world class products. I love web, Javascript and F.R.I.E.N.D.S.

Discussion

markdown guide
 

There is a cool library available for cloning objects efficiently called rfdc (Really Fast Deep Clone).

const clone = require("rfdc")();

const user = {
  id: 1,
  name: "Leanne Graham",
  username: "Bret",
  email: "Sincere@april.biz",
  phone: "1-770-736-8031 x56442",
  website: "hildegard.org",
  company: {
    name: "Romaguera-Crona",
    catchPhrase: "Multi-layered client-server neural-net",
    bs: "harness real-time e-markets",
  },
};

const nextUser = clone(user);

console.log(user === nextUser); // This will return false because object have differnt reference.
 

The great thing about rfdc is that it super quickly clones real objects with cycles/functions/constructors etc. It is super quick at that.

If you need to quick clone something including enumerable properties but NOT any of that cool stuff rfdc does - then I use this that is a little quicker if you know that there won't be issues:

export function clone(o) {
    var newO, i

    if (typeof o !== 'object') {
        return o
    }
    if (!o) {
        return o
    }

    if (Array.isArray(o)) {
        newO = []
        for (i = 0; i < o.length; i += 1) {
            newO[i] = clone(o[i])
        }
        return newO
    }

    newO = {}
    for (i in o) {
        newO[i] = clone(o[i])
    }
    return newO
}

 

Thanks, Mike for sharing this.

 
 
 

Thanks for such a cool suggestion.
Infact after your comment, I have looked into it and its comparision with other libraries. I think it's very efficient.

 
 

Great article. Many of us encounter this problem and don't know how to resolve it. I've personally encountered it once or twice.

 

So there is a lodash library which gives cloneDeep() method to perfectly deep copy an object. Below an example attached:

var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);

You can also visit this page to find more: lodash.com/docs/4.17.15#cloneDeep

Another approach is to use recursion.
make a custom function like one I have used to iterate on each key in object and if that key is also an object then again call the method to copy the keys.
Something like below:


function isObject(obj) {
  let type = typeof obj;
  return type === 'object';
};

function deepCopy(obj) {
  let copiedObj = {};
  for (let key in obj) {
    if (obj.hasOwnkeyerty(key)) {
      if (isObject(obj[key])) {
        copiedObj[key] = deepCopy(obj[key]);
      } else {
        copiedObj[key] = obj[key];
      }
    }
  }
  return copiedObj;
}
 

I surely will use the recursion approach too. However, typeof is sloppy in terms of differentiating between array and object. In order to truly check for object type, I will go with plain old trick below:

function isObject(value) {
  return Object.prototype.toString.call(value) === '[object Object]';
}

Cool. I missed that part. Thanks for the solution.

I was just giving some information about how can we make such a function to deep copying with help of recursion. So, there might be some more improvements that can be implemented to make it handle every case.

But Great Insight!

 

I have stopped using lodash. I will use the recursive function

 

Great! make your own common function to handle all cases.

Enjoy!

 

Great article!
Just last week i encountered this same issue with some production code:
we had a video player that accepts some options when you initialize it; for some strange reasons if 2 video player where initialized on the same page, they both had the same value for some options passed only to the second player. The code is legacy, the videoplayer is implemented as a big fat object.
It turns out that the player store options for a particular feature in a nested object, so even if i clone the player itself with Object.assign(), the inner object is the same reference for both of them 😅 ...

 

😀 Interesting scenario!
I encountered this problem when I was passing down some data to child components but somehow it was changing data in parent component too.

Thanks for reading!

 

Great article with great solution.