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;
};
asked Apr 6, 2021 at 8:18
\$\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\$ Commented Apr 8, 2021 at 12:18
  • \$\begingroup\$ Did you have a look at this working example? \$\endgroup\$ Commented Apr 8, 2021 at 12:33
  • \$\begingroup\$ Yes, it throws an error on a cyclical object. JSON.stringify falls over. \$\endgroup\$ Commented Apr 8, 2021 at 12:45
  • \$\begingroup\$ Ok, so it's not suitable for cyclic structures, thnx for pointing that out. \$\endgroup\$ Commented Apr 8, 2021 at 13:13
  • \$\begingroup\$ Yes, can you also fix this question and add isImmutable, cloneArr etc. \$\endgroup\$ Commented 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..
answered Apr 12, 2021 at 15:04
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

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

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.