Custom Deep Equal
intermediateStructural equality for nested data: type checks, key comparison, and recursion done right.
Implement deepClone in JavaScript with WeakMap cycle handling, Date/RegExp/Map/Set support, prototype preservation — and when to just use structuredClone.
Implement
deepClone(value): a fully independent copy — mutate the clone anywhere, the original never notices. Handle nested objects/arrays,Date,Map,Set, and circular references.
This question is a recursion-plus-type-dispatch drill with one famous trap (cycles) and one senior-level kicker: knowing exactly where your clone lies to you — because every deep clone, including the platform's, has a boundary, and naming yours is worth more than handling one more type.
function deepClone(value, visited = new WeakMap()) {
// Primitives (and functions — see the limits section) pass through
if (value === null || typeof value !== "object") {
return value;
}
// Cycles & shared references: if we've cloned this object on this walk,
// return THAT clone — same trick as stringify's ancestor set, but as a
// mapping so shared subtrees stay shared instead of duplicating.
if (visited.has(value)) {
return visited.get(value);
}
if (value instanceof Date) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) {
const clone = new Map();
visited.set(value, clone);
for (const [k, v] of value) {
clone.set(deepClone(k, visited), deepClone(v, visited)); // keys clone too
}
return clone;
}
if (value instanceof Set) {
const clone = new Set();
visited.set(value, clone);
for (const v of value) clone.add(deepClone(v, visited));
return clone;
}
if (Array.isArray(value)) {
const clone = [];
visited.set(value, clone);
value.forEach((item, i) => { clone[i] = deepClone(item, visited); });
return clone;
}
// Plain-ish objects: keep the prototype so class instances stay instances
const clone = Object.create(Object.getPrototypeOf(value));
visited.set(value, clone);
for (const key of Reflect.ownKeys(value)) { // string AND symbol keys
clone[key] = deepClone(value[key], visited);
}
return clone;
}Register the clone in visited before recursing into children. That's the entire cycle solution: when the walk comes back around to a self-reference, the half-built clone is already in the map and gets returned, wiring the cycle into the copy exactly as it was in the original:
const original = { name: "root" };
original.self = original;
const clone = deepClone(original);
clone.self === clone; // true — the cycle points at the CLONE
clone.self === original; // false — no back-door to the original
const shared = { hits: 0 };
const doc = { a: shared, b: shared };
const copy = deepClone(doc);
copy.a === copy.b; // true — shared stays shared (a WeakMap *mapping*
// buys this; a visited *set* that just throws wouldn't)The WeakMap (vs Map) choice is one sentence: keys are objects, held weakly, so the bookkeeping can't extend any object's lifetime — the same reasoning as WeakMap-keyed memoization.
Every deep clone has a boundary. Yours:
structuredClone refuses outright and throws.)clone[key] = value[key] evaluates accessors (the same Get/Set semantics as Object.assign); cloning descriptors verbatim needs getOwnPropertyDescriptors and a decision about whether a live getter on a clone even makes sense.File, streams) — instanceof-dispatch can't reconstruct engine-internal state; DOM nodes have their own cloneNode.Object.create(getPrototypeOf(...)) line keeps methods working, but #private fields are installed only by the constructor and are invisible to reflection: the clone has the prototype, not the private state. Very few candidates know this; it's a great volunteered detail.JSON.parse(JSON.stringify(x)) | structuredClone(x) | this implementation | |
|---|---|---|---|
| Cycles | throws | ✓ | ✓ |
Date | → string ❌ | ✓ | ✓ |
Map/Set | → {} ❌ | ✓ | ✓ |
undefined/functions in objects | silently dropped | functions throw | kept / shared by ref |
| Prototypes / class instances | lost | lost | preserved |
| Getters | evaluated | evaluated | evaluated |
The modern default is structuredClone — but note the row it loses: it does not preserve prototypes (a class Point instance comes back a plain object). "I'd reach for structuredClone unless class instances matter, and here's the from-scratch version because interviews and prototype preservation both exist" is the complete answer.
Reflect.ownKeys picks up symbols; full descriptor fidelity is the getOwnPropertyDescriptors extension — know the API name even if you don't write it.forEach skips holes, so holes stay holes (matching reduce's hole semantics); an index for loop would densify them with undefined.Map keys — cloning values but not keys is the common half-measure; whether object keys should be cloned (breaking identity-keyed lookups!) is a genuinely good design argument to have out loud.visited after recursing — infinite loop on the first cycle.Date/RegExp branches — they fall into the plain-object path and come out as {} with the wrong prototype.Object.keys + {} — silently strips prototypes and symbol keys without being able to say so (doing it knowingly as a "plain data only" contract is fine — the crime is not knowing).JSON.parse(JSON.stringify(...)) as deep clone without its failure table.depth/shouldClone(key) through the recursion — turns the drill into an API-design conversation.postMessage); the full comparison.Structural equality for nested data: type checks, key comparison, and recursion done right.
When to use the built-in structuredClone, where it fails (functions, prototypes), and how it compares to JSON round-tripping.
Flatten arbitrarily nested arrays recursively and iteratively — then compare with Array.prototype.flat.
Turn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.