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;
};
1 Answer 1
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
asconst 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..
JSON.stringify
falls over. \$\endgroup\$isImmutable
,cloneArr
etc. \$\endgroup\$