DEV Community

Cover image for Be aware when cloning objects in JavaScript! 👯‍♀️
Chris Bongers
Chris Bongers

Posted on • Originally published at daily-dev-tips.com

Be aware when cloning objects in JavaScript! 👯‍♀️

Now and then you'll need to clone an object in JavaScript, mainly one to be modified but you still want the original to stay the same.

Let's say for this purpose we have a duplicate and change function.

Meaning we have an object, which we will duplicate and then change.
The original, of course, will need to stay the same.

Benchmark JavaScript clone

In JavaScript we can ofcourse clone a object by assigning it to a new const like this:

const original = { color: '🔴', child: { action: 'stop' } };
const clone = original;

console.log(original); // {color: "🔴", child: {action: "stop"}}
console.log(clone); // {color: "🔴", child: {action: "stop"}}
Enter fullscreen mode Exit fullscreen mode

Ok wow that works, cool done!

But not really, here comes the issue with this.

clone.color = '🟢';
console.log(original); // {color: "🟢", child: {action: "stop"}}
console.log(clone); // {color: "🟢", child: {action: "stop"}}
Enter fullscreen mode Exit fullscreen mode

Hmm that's not cool, now our original one is also modified!

This is caused because of objects being reference types. When we choose to use = to clone, we make a pointer to object one instead of actually cloning it.

Clone using the spread operator

The spread operator is introduced in ES6 so fairly new, and not in the official specs yet!

To use it, you create a new object prefixed by three dots ...

const original = { color: '🔴', child: { action: 'stop' } };
const spread = {...original};
console.log(original); // { color: '🔴', child: { action: 'stop' } }
console.log(spread); // { color: '🔴', child: { action: 'stop' } }
Enter fullscreen mode Exit fullscreen mode

Ok, our basic clone works again, now let's check the change

spread.color = '🟢';
console.log(original); // { color: '🔴', child: { action: 'stop' } }
console.log(spread); // { color: '🟢', child: { action: 'stop' } }
Enter fullscreen mode Exit fullscreen mode

Yes, we did it!

But wait let's make sure we can also modify the child element.

spread.color = '🟢';
spread.child.action = 'start';
console.log(original); // { color: '🔴', child: { action: 'start' } }
console.log(spread); // { color: '🟢', child: { action: 'start' } }
Enter fullscreen mode Exit fullscreen mode

Wait, what just happened?

This is because the spread operator is a shallow copy, so only the first level will be actually copied, the rest will be assigned.

Clone in JavaScript using Object.assign

This is by far the method you will read the most about. It's basically the old version of the spread operator.

You can use it as follows:

const original = { color: '🔴', child: { action: 'stop' } };
const assign = Object.assign({}, original);
console.log(original); // { color: '🔴', child: { action: 'stop' } }
console.log(assign); // { color: '🔴', child: { action: 'stop' } }
Enter fullscreen mode Exit fullscreen mode

Cool, this also clones, but let's check if it modifies correctly.

assign.color = '🟢';
assign.child.action = 'start';
console.log(original); // { color: '🔴', child: { action: 'start' } }
console.log(assign); // { color: '🟢', child: { action: 'start' } }
Enter fullscreen mode Exit fullscreen mode

Damn, still the same issue turns out Object.assign is also a shallow copy.

So now what?

Using JSON to clone

A quick and dirty hack to deep-clone is using JSON to stringify and then parse the object again.

This is not a "best-practice" but used by many people, and good enough for basic cloning.

For more robust deep-clone make use of packages like lodash cloneDeep.

It works like this:

const original = { color: '🔴', child: { action: 'stop' } };
const json = JSON.parse(JSON.stringify(original));
console.log(original); // { color: '🔴', child: { action: 'stop' } }
console.log(json); // { color: '🔴', child: { action: 'stop' } }
Enter fullscreen mode Exit fullscreen mode

Awesome, does the exact same thing, but let's see when we modify data.

json.color = '🟢';
json.child.action = 'start';
console.log(original); // { color: '🔴', child: { action: 'stop' } }
console.log(json); // { color: '🟢', child: { action: 'start' } }
Enter fullscreen mode Exit fullscreen mode

Yes, we did it! A fully cloned object that we can modify!

Thank you for reading, and let's connect!

Thank you for reading my blog. Feel free to subscribe to my email newsletter and connect on Facebook or Twitter

Top comments (17)

Collapse
 
lexlohr profile image
Alex Lohr

If you want to do it yourself for simple cases, you can just use recursion:

const cloneObject = (obj) => 
  Array.isArray(obj)
  ? obj.map(cloneObject)
  : ({}).toString.call(obj) === '[object Object]'
  ? Object.fromEntries(
        Object.entries(obj).map(([key, val]) => [key, cloneObject(val)])
      )
  : o
Enter fullscreen mode Exit fullscreen mode

But beware of cyclic references, i.e.

const x = []
x.push(x)
cloneObject(x) // Uncaught RangeError: Maximum call stack size exceeded
Enter fullscreen mode Exit fullscreen mode

To avoid that, you'll need to store references, for which a Map is easily the best possible option, but you need to make sure you fill the map with a reference/clone pair before you start the recursion:

const cloneObject = (obj, map) => {
  if (map === undefined) {
    map = new Map()
  }
  if (map.has(obj)) {
    return map.get(obj)
  }
  if (Array.isArray(obj)) {
    const clone = []
    map.set(obj, clone)
    obj.forEach((value, index) => clone[index] = cloneObject(value, map))
    return clone
  }
  if (({}).toString.call(obj) === '[object Object]') {
    const clone = {}
    map.set(obj, clone)
    Object.entries(obj).forEach(
      ([key, value]) => { clone[key] = cloneObject(value, map) }
    )
    return clone
  }
  return obj
}
Enter fullscreen mode Exit fullscreen mode

Now cyclic references are no longer an issue:

const x = []
x.push(x)
cloneObject(x) // new Array containing a reference to itself
Enter fullscreen mode Exit fullscreen mode

That still doesn't handle stuff like constructed objects, though (Date, Headers, etc.).

Collapse
 
dailydevtips1 profile image
Chris Bongers • Edited

Hey, that basically will do the same as the JSON method right?

This is one way to sort of deep-clone, haven't tested this fully:

const original = { color: '🔴', child: { action: 'stop', date: new Date() } };

function clone(obj) {
    if (obj === null || typeof (obj) !== 'object')
        return obj;

    if (obj instanceof Date)
        var temp = new obj.constructor();
    else
        var temp = obj.constructor();

    for (var key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            temp[key] = clone(obj[key]);
        }
    }
    return temp;
}
const cloned = clone(original);

cloned.color = '🟩';
console.log(original);
console.log(cloned);
console.log(cloned.child.date.getFullYear());
Enter fullscreen mode Exit fullscreen mode

Response:

'🟩'
{
  color: '🔴',
  child: { action: 'stop', date: 2020-11-03T13:42:36.334Z }
}
{
  color: '🟩',
  child: { action: 'stop', date: 2020-11-03T13:42:36.334Z }
}
2020
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lexlohr profile image
Alex Lohr • Edited

Yes and no. JSON.parse/stringify will serialize and deserialize the data. Some types are not supported and cyclic references will also lead to errors. But putting both variants together, you'll get:

const clone = (obj, map) => {
    if (!map) {
        map = new Map();
    }
    if (obj === null || typeof (obj) !== 'object')
        return obj;

    if (map.has(obj))
        return map.get(obj);

    const temp = obj instanceof Date
        ? new obj.constructor()
        : obj.constructor();

    map.set(obj, temp);

    for (let key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
            temp[key] = clone(obj[key], map);
        }
    }
    return temp;
}
Enter fullscreen mode Exit fullscreen mode

Now that works for all usual types. It will fail for some of the modern things like Map/WeakMap etc.

Thread Thread
 
dailydevtips1 profile image
Chris Bongers

It all goes deeper than you would think, wonder why they don't build this in ECMAScript...
Seems like a pretty "basic" function to deepclone right?

Thread Thread
 
lexlohr profile image
Alex Lohr

I guess libraries like lodash made it convenient enough to have that functionality, so there was no reason to provide a native method. Also, since the native types are currently a moving target, it seems prudent to wait until it has stabilized enough to make such a functionality feasible. That shouldn't stop anybody from making your own proposal to the TC39 committee to include Object.clone(obj) into a future ECMAScript standard, though.

Thread Thread
 
lexlohr profile image
Alex Lohr

I guess I'll make a proposal myself, if nobody beats me to it. Here's the polyfill:

if (typeof Object.clone !== "function") {
  const clone = (obj, map) => {
    if (obj === null || typeof obj !== "object" || obj instanceof WeakMap)
      return obj;

    if (map.has(obj)) return map.get(obj);

    const temp =
      obj instanceof TypedArray
        ? new obj.constructor(obj.length)
        : new obj.constructor();

    map.set(obj, temp);

    if (obj instanceof TypedArray) {
      temp.set(obj.map((value) => clone(value, map)));
    } else if (obj instanceof Map) {
      obj.forEach((value, key) => temp.set(key, clone(value, map)));
    } else if (obj instanceof Date) {
      temp.setTime(obj.getTime());
    } else {
      for (const key in obj) {
        if (Object.prototype.hasOwnProperty.call(obj, key)) {
          temp[key] = clone(obj[key], map);
        }
      }
    }
    return temp;
  };
  Object.clone = (obj) => clone(obj, new Map());
}
Enter fullscreen mode Exit fullscreen mode

I also did a small test suite and a documentation. I'll work a bit on it and then release it to the public.

Thread Thread
 
dailydevtips1 profile image
Chris Bongers

Wow Alex, Your a speedy guy!
Nice work, happy to test with you 👀

Thread Thread
 
lexlohr profile image
Alex Lohr • Edited

Here's the initial draft: github.com/atk/object-clone-proposal. Feedback is appreciated.

Collapse
 
c7tt8nt2p profile image
c7tt8nt2p

Nice article, but be aware of

JSON.parse(JSON.stringify(x));
Enter fullscreen mode Exit fullscreen mode

might not work as expected with JS Date object, for example:

var date = new Date();
var x = JSON.parse(JSON.stringify(date));
console.log(date.getFullYear()); // 2020
console.log(x.getFullYear()); // error: Uncaught TypeError: x.getFullYear is not a function
Enter fullscreen mode Exit fullscreen mode

The solution I can think of is instantiating a new Date() and pass the value in the constructor

Collapse
 
dailydevtips1 profile image
Chris Bongers

Ah yes good you mention this, certain objects get destroyed in the JSON conversion!

In this case, you would be better of making a custom deep clone or use one of the tools mentioned.
For more "flat" objects I tend to use JSON.parse, but yes good point!

Collapse
 
toqeer__abbas profile image
Toqeer Abbas

Love it... great work

Collapse
 
dailydevtips1 profile image
Chris Bongers

Thank you Toqueer, Glad you like it!

Collapse
 
toqeer__abbas profile image
Toqeer Abbas

Welcome

Collapse
 
khizerrehandev profile image
khizerrehandev

Great Article. What is the best way to deep copy in vanilla javascript? Thanks

Collapse
 
dailydevtips1 profile image
Chris Bongers

Hey jsforlife,

It does kind of depend on what object you are cloning, I tend to use the JSON.parse solution quite often.
It's the quickest deep clone and for simple objects works really well.

For my "flat" objects see the comment above dates for instance won't work.

If you are using plugins, stick with the loDash deep clone, that solves it for you.

Collapse
 
abhidj0090 profile image
abhidj0090

Great article man,

Have faced this issue plenty of times and have ended with different solutions every time :( and i still don't understand why object.assign() doesn't work

Collapse
 
dailydevtips1 profile image
Chris Bongers

Hey,

Yes you would expect by now the Object.assign would be a deepClone, OR that JavaScript by now would have a perfect DeepClone function build in!