DEV Community

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

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.