Custom Deep Clone
advancedRecursively clone nested objects and arrays, handling cycles with a WeakMap.
Compare structuredClone, JSON.parse(JSON.stringify()), and hand-written deep clone — what each handles, where each fails, and which to choose in interviews.
"Deep clone this object" used to mean writing recursion. Today the strongest answer starts differently: "Can I use structuredClone?" — then demonstrates you know exactly what it does and where it fails. This page compares the three approaches so you can pick deliberately and defend the choice.
const original = {
date: new Date(),
pattern: /abc/gi,
buffer: new Map([["a", new Set([1, 2])]]),
nested: { deep: [1, [2, [3]]] },
};
original.self = original; // circular!
const copy = structuredClone(original);
copy.self === copy; // true — cycle preserved
copy.date instanceof Date; // true
copy.buffer.get("a") instanceof Set; // truestructuredClone uses the same structured-clone algorithm as postMessage/IndexedDB. It handles what JSON never could:
Date, RegExp, Map, Set, ArrayBuffer, typed arrays, Blob, File, Error.structuredClone({ fn: () => {} }); // ❌ DataCloneError — functions
structuredClone({ el: document.body }); // ❌ DataCloneError — DOM nodes
structuredClone(new (class X {})()); // ⚠️ becomes plain object — prototype lost
const o = { get computed() { return 42; } };
structuredClone(o).computed; // 42 — but now a plain data propertyFour limits to recite: no functions, no DOM nodes, prototypes are dropped (class instances flatten to plain objects), and getters are evaluated, not copied. Symbols and property descriptors are also lost. If state contains methods or class instances, structuredClone silently isn't enough.
The old one-liner is now mostly an interview trap:
const copy = JSON.parse(JSON.stringify(original));Failure modes (know them cold — enumerated in depth in custom JSON.stringify):
TypeError: Converting circular structure).undefined, functions, and symbols are dropped (or nulled in arrays).Date becomes a string; Map/Set become {}; NaN/Infinity become null.Acceptable only for known-JSON-safe data (API payloads, config). If you offer it, say those caveats in the same breath.
When the interviewer says "no built-ins," you're being tested on recursion, type dispatch, and cycle handling — the full implementation with a WeakMap lives in custom deep clone. The skeleton:
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== "object") return value; // primitives + functions
if (seen.has(value)) return seen.get(value); // cycles
let result;
if (value instanceof Date) result = new Date(value);
else if (value instanceof RegExp) result = new RegExp(value.source, value.flags);
else if (value instanceof Map) {
result = new Map();
seen.set(value, result);
for (const [k, v] of value) result.set(deepClone(k, seen), deepClone(v, seen));
return result;
} else if (value instanceof Set) {
result = new Set();
seen.set(value, result);
for (const v of value) result.add(deepClone(v, seen));
return result;
} else if (Array.isArray(value)) {
result = [];
seen.set(value, result);
value.forEach((v, i) => (result[i] = deepClone(v, seen)));
return result;
} else {
result = Object.create(Object.getPrototypeOf(value)); // preserve the prototype!
seen.set(value, result);
for (const key of Reflect.ownKeys(value)) {
result[key] = deepClone(value[key], seen);
}
return result;
}
return result;
}Note the two things a custom clone can do that structuredClone can't: preserve prototypes (Object.create(Object.getPrototypeOf(value)) — see prototypal inheritance) and decide policy for functions (share the reference rather than throw).
| Requirement | structuredClone | JSON round-trip | Custom clone |
|---|---|---|---|
| Circular references | ✅ | ❌ throws | ✅ with WeakMap |
| Date / Map / Set / RegExp | ✅ | ❌ corrupted | ✅ if you code it |
| Functions in the graph | ❌ throws | ❌ dropped | ✅ your policy |
| Class instances (prototype) | ❌ flattened | ❌ flattened | ✅ preserved |
| Performance on big graphs | Fast (native) | Slow (string) | Depends |
| Availability | All modern runtimes | Everywhere | Everywhere |
"Default to
structuredClone— native, handles cycles and built-ins. If the object holds functions or class instances, I need a custom clone with a WeakMap for cycles and prototype preservation — here's how I'd write it. JSON round-trip only for known JSON-safe data, and here's everything it silently breaks."
Three tools, trade-offs stated, then implement the one they ask for. Follow-ups worth anticipating: shallow vs deep ({ ...obj } copies one level; nested objects are shared references) and cloning across realms (structured clone is also the algorithm used to send data to workers via postMessage).
Recursively clone nested objects and arrays, handling cycles with a WeakMap.
Structural equality for nested data: type checks, key comparison, and recursion done right.
Flatten arbitrarily nested arrays recursively and iteratively — then compare with Array.prototype.flat.
Use a Proxy to support arr[-1] like Python — a practical introduction to Proxy traps.