1
\$\begingroup\$

I have written a (recursive) deep clone method for ES/JS-objects. It seems to work fine, but maybe I'm missing things. Please, feel free to comment!

Note: the method will not clone cyclic structures. If it should be somehow necessary to be able to clone such structures, maybe Crockfords cycle.js can be used - but it will mangle the structure.

initializeObjCloner();
const log = Logger();
// initial object
const test1 = {
  foo: [1, 2 ,3],
  bar: { foo: {bar: 5}, foobar: "foobar", bar2: {bar3: {foo: 42}} },
};
// clone it
const test2 = Object.clone(test1);
// change a few props to demonstrate
// test2 not being a reference to test1
test2.bar.foo.foobar = "barfoo";
test2.bar.bar2.bar3.foo = 43;
test2.foo = test2.foo.map(v => v + 10);
test2.test2Only = "not in test1";
log (`**Test1:`, test1, `\n**Test2:`, test2);

// error on cyclic structures
const c = {hello: "world"};
c.recycled = c;
log(`\n${Object.clone(c)}`);


function initializeObjCloner() {
  const isImmutable = val =>
    val === null || 
    val === undefined || 
    [String, Boolean, Number].find(V => val.constructor === V);
  const isObject = obj =>
    (obj.constructor !== Date && 
    JSON.stringify(obj) === "{}") || 
    Object.keys(obj).length;
  const cloneArr = arr => arr.reduce( (acc, value) => 
    [...acc, isObject(value) ? cloneObj(value) : value], []);
  const isCyclic = obj => {
    try {
      JSON.stringify(obj);
    } catch(err) {
      return err.message;
    }
    return null;
  };
  // --------------------------
  // The actual cloning method
  // --------------------------
  const cloneObj = (obj) => {
    const cyclic = isCyclic(obj);
    return cyclic ? 
      `Object.clone error: Cyclic structures can not be cloned, sorry.` :
      Object.keys(obj).length === 0 ? obj :
        Object.entries(obj)
        .reduce( (acc, [key, value]) => ( {
            ...acc,
            [key]: 
              value instanceof Array 
              ? cloneArr(value) :
                !isImmutable(value) && isObject(value)
                  ? cloneObj(value)
                  : value && value.constructor
                    ? new value.constructor(value)
                    : value } ),  {} );
  };
  Object.clone = cloneObj;
}

function Logger() {
  const report =
    document.querySelector("#report") ||
    document.body.insertAdjacentElement(
      "beforeend",
      Object.assign(document.createElement("pre"), { id: "report" })
    );
  return (...args) =>
    args.forEach(
      arg =>
        (report.textContent +=
          (arg instanceof Object ? JSON.stringify(arg, null, 2) : arg) + "\n")
    );
}
body {
  font: normal 12px/15px verdana, arial, sans-serif;
  margin: 2rem;
}

if readability is an issue, cloneObj may also be written as:

function cloneObj(obj) {
  if (Object.keys(obj).length < 1) { return obj; }
  
  if (obj.constructor === Date) {
    return new Date(obj);
  }
  
  if (obj.constructor === Array) {
    return cloneArr(obj);
  }

  let newObj = {};

  for ( let [key, value] of Object.entries(obj) ) {
    if (!isImmutable(value) && isObject(value)) {
      newObj[key] = cloneObj(value);
    }

    if (!newObj[key] && value && value.constructor) {
      newObj[key] = new value.constructor(value);
    }
    
    if (!newObj[key]) {
      newObj[key] = value;
    }
  }
  return newObj;
};
\$\endgroup\$
7
  • \$\begingroup\$ You are missing several small functions, making this code not work without them. This is grounds to get this question closed on CodeReview. Also, this code cannot handle cyclic structures, which are sadly more common than you would think. \$\endgroup\$
    – konijn
    Apr 8, 2021 at 12:18
  • \$\begingroup\$ Did you have a look at this working example? \$\endgroup\$
    – KooiInc
    Apr 8, 2021 at 12:33
  • \$\begingroup\$ Yes, it throws an error on a cyclical object. JSON.stringify falls over. \$\endgroup\$
    – konijn
    Apr 8, 2021 at 12:45
  • \$\begingroup\$ Ok, so it's not suitable for cyclic structures, thnx for pointing that out. \$\endgroup\$
    – KooiInc
    Apr 8, 2021 at 13:13
  • \$\begingroup\$ Yes, can you also fix this question and add isImmutable, cloneArr etc. \$\endgroup\$
    – konijn
    Apr 8, 2021 at 13:15

1 Answer 1

2
\$\begingroup\$

I spent a ton of time on this;

  • The choice to replicate Date but not the other 50+ built-ins is interesting
  • I would write isObject as const isObject = x => (typeof x === 'object' && x !== null)
  • A question to ask yourself, what about functions, do you want to clone those as well?
  • if (Object.keys(obj).length < 1) { return obj; } is interesting, this means you will allow for modifications in the original object to impact the new object
  • The poor man's deep clone for non-cyclic structures is JSON.parse(JSON.stringify(o))
  • After the construction here: newObj[key] = new value.constructor(value); I would still copy over the properties as well
  • Still mulling over a counter example..
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.