Last active
April 12, 2022 09:35
-
-
Save StephenChips/7dc1c9618f7a65e36c0e83c5eff6d395 to your computer and use it in GitHub Desktop.
Deep-clone JavaScript objects.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @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