DEV Community

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

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

Chris Bongers on November 03, 2020

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 f...
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!