Skip to content

Instantly share code, notes, and snippets.

@StephenChips
Last active April 12, 2022 09:35
Show Gist options
  • Save StephenChips/7dc1c9618f7a65e36c0e83c5eff6d395 to your computer and use it in GitHub Desktop.
Save StephenChips/7dc1c9618f7a65e36c0e83c5eff6d395 to your computer and use it in GitHub Desktop.
Deep-clone JavaScript objects.
/**
* @author StephenChips
*
* Notes:
* 1. There is no need to have a stack if the function is recursive.
* 2. To ensure every object only has one copy, you have to use a Map to map the
* objects to their copies.
* 3. Unlike toJSON, function deepClone should be able to handle cyclic objects.
* 4. Some object cannot be clone (or doesn't have a relaible method) and should just
* return themselves directly.
*/
function deepClone(obj) {
// To store objects and their copy.
const copyMap = new Map();
return deepCloneHelper(obj);
function deepCloneHelper(obj) {
// This not only excludes primitives, but also functions.
if (typeof obj != "object" || obj == null) return obj;
// WeakMap/Set are not iterable, therefore cannot be copied.
if (obj instanceof WeakMap) return obj;
if (obj instanceof WeakSet) return obj;
if (obj instanceof Number) return new Number(obj);
if (obj instanceof Boolean) return new Boolean(obj);
if (obj instanceof String) return new String(obj);
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Node) return obj.cloneNode(true);
if (obj instanceof Map) return new Map(obj);
if (obj instanceof Set) return new Set(obj);
/**
* The `copyMap` contains objects that have been visited and
* those that is still on the stack.
*/
let copy = copyMap.get(obj);
if (copy) return copy;
/**
* Even though we are just start constructing the copy, we should
* put it into the copyMap. Because if some other nodes refer to
* it, they will know there has been a copy created, even it's under
* construction. Then they will reuse the copy instead of creating
* a new one.
*/
if (Array.isArray(obj)) {
copy = [];
copyMap.set(obj, copy);
for (const item of obj) {
copy.push(deepCloneHelper(item));
}
} else {
copy = Object.create(Object.getPrototypeOf(obj));
copyMap.set(obj, copy);
const namedProps = Object.getOwnPropertyNames(obj);
for (const prop of namedProps) {
copyProperty(obj, copy, prop);
}
const symbolProps = Object.getOwnPropertySymbols(obj);
for (const prop of symbolProps) {
copyProperty(obj, copy, prop);
}
}
return copy;
}
function copyProperty(srcObj, tgtObj, prop) {
let descriptor = Object.getOwnPropertyDescriptor(srcObj, prop);
if ("value" in descriptor) {
descriptor.value = deepCloneHelper(srcObj[prop]);
}
Object.defineProperty(tgtObj, prop, descriptor);
}
}
const testObj = {
date: new Date(),
node: document.body,
bool: false,
num: 3,
boolObj: new Boolean(false),
str: "str",
strObj: new String("str"),
emptyObj: {},
obj: {
b: {},
c: {},
d: [false, {}, [], [1, 3, 4]]
},
reg: /a(.*)b+c/gi,
[Symbol()]: 3,
map: new Map(),
set: new Set(),
};
testObj.obj.b.emptyObj = testObj.emptyObj;
testObj.obj.d.push(testObj.emptyObj);
Object.defineProperty(testObj, "unenum", {
value: 3,
writable: false,
enumerable: false
});
console.log(deepClone(testObj));
const cycle = {};
cycle.cycle = cycle;
console.log(deepClone(cycle));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment