Topics
v5.0.0
On this page · sections

JavaScript Deep Clone & Equality

Can you copy and compare a graph of objects — cycles, Date, Map — without sharing a nested reference by accident?

Level: Medium · Coding Round · Object Graphs

Why This Question Matters

  • Spread is not a deep copy. Nested mutation leaks into the original.
  • Interviewers want graph traversal, not JSON.parse(JSON.stringify).
  • Cycles, built-ins, and “register the clone before recurse” are the senior signals.
  • React usually wants identity, not an O(n) deep equal on every render.

Five problems. Same seen map.

Problem 1 — deepClone

Question: Implement deepClone(value).

Description: Return a new object graph that is structurally the same as value, but shares no nested references with it. Mutating the clone must not change the source. Primitives stay as-is. Recurse into plain objects and arrays. A Date must remain a Date. Cyclic graphs must clone without infinite recursion — register each new clone in a WeakMap before walking children. Do not use JSON.parse(JSON.stringify).

function deepClone(value) {}

Example

const original = { nested: { x: 1 } }; const shallow = { ...original }; shallow.nested.x = 9; original.nested.x; // 9 — nested object is shared const copy = deepClone(original); copy.nested.x = 2; original.nested.x; // 1
obj.self = obj Register clone first, then copy self → same clone Otherwise: obj → self → obj → …

Solution

function deepClone(value, seen = new WeakMap()) { if (value === null || typeof value !== "object") { return value; } if (seen.has(value)) { return seen.get(value); } if (value instanceof Date) { const clone = new Date(value.getTime()); seen.set(value, clone); return clone; } if (Array.isArray(value)) { const clone = []; seen.set(value, clone); value.forEach((item, i) => { clone[i] = deepClone(item, seen); }); return clone; } const clone = {}; seen.set(value, clone); for (const key of Object.keys(value)) { clone[key] = deepClone(value[key], seen); } return clone; }

The important line is seen.set(value, clone) before recursion.

Use case

Form state, config, API payloads you will mutate.

Follow-ups

Q. Why WeakMap?

Answer: Keys must be objects. It does not keep the source graph alive after the clone finishes.

Q. JSON clone?

Answer: Date becomes a string. Set becomes {}. Functions drop. Cycles throw.

Problem 2 — Clone Map, Set, RegExp

Question: Extend deepClone so Map, Set, and RegExp keep their type.

Description: Enumerable keys on a Map are empty, so treating built-ins as plain objects is wrong. Nested values (and Map keys) must be cloned. Cycles inside these structures must still resolve through the same seen map. Prefer Object.prototype.toString.call over instanceof when types may cross iframes. Map.forEach is (value, key), not (key, value).

function deepCloneExtended(value) {}

Example

const src = new Map([["user", { n: 1 }]]); const copy = deepCloneExtended(src); copy.get("user").n = 9; src.get("user").n; // 1

Solution

function tag(value) { return Object.prototype.toString.call(value); } function deepCloneExtended(value, seen = new WeakMap()) { if (value === null || typeof value !== "object") { return value; } if (seen.has(value)) { return seen.get(value); } switch (tag(value)) { case "[object Date]": { const clone = new Date(value.getTime()); seen.set(value, clone); return clone; } case "[object RegExp]": { const clone = new RegExp(value.source, value.flags); clone.lastIndex = value.lastIndex; seen.set(value, clone); return clone; } case "[object Map]": { const clone = new Map(); seen.set(value, clone); value.forEach((v, k) => { clone.set(deepCloneExtended(k, seen), deepCloneExtended(v, seen)); }); return clone; } case "[object Set]": { const clone = new Set(); seen.set(value, clone); value.forEach((v) => { clone.add(deepCloneExtended(v, seen)); }); return clone; } case "[object Array]": { const clone = []; seen.set(value, clone); value.forEach((item, i) => { clone[i] = deepCloneExtended(item, seen); }); return clone; } default: { const clone = {}; seen.set(value, clone); for (const key of Object.keys(value)) { clone[key] = deepCloneExtended(value[key], seen); } return clone; } } }

Enumerable keys on a Map are empty. Treat built-ins as their own containers.

Use case

Caches and stores that hold Map / Set, not only JSON.

Follow-ups

Q. Production?

Answer: structuredClone() when the types match. It still cannot clone functions.

Problem 3 — deepMerge

Question: Implement deepMerge(target, source).

Description: Return a new object. Do not mutate the inputs. When both values at a key are plain objects, merge recursively. Otherwise source replaces target (clone the replacement). Arrays and Date replace; they are not concatenated or field-merged. null replaces. Ignore __proto__, constructor, and prototype. Spread is not enough: replacing a whole theme object drops keys that only existed on the default.

function deepMerge(target, source) {}

Example

deepMerge( { theme: { dark: true }, n: 1 }, { theme: { compact: true }, n: 2 } ); // { theme: { dark: true, compact: true }, n: 2 } deepMerge({ list: [1] }, { list: [2, 3] }); // { list: [2, 3] }
Both plain objects? → recurse Otherwise → source replaces (cloned)

Solution

function isPlainObject(value) { if (value === null || typeof value !== "object") { return false; } const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } const UNSAFE = new Set(["__proto__", "constructor", "prototype"]); function deepMerge(target, source) { if (!isPlainObject(target) || !isPlainObject(source)) { return deepCloneExtended(source); } const out = {}; for (const key of Object.keys(target)) { if (!UNSAFE.has(key)) { out[key] = deepCloneExtended(target[key]); } } for (const key of Object.keys(source)) { if (UNSAFE.has(key)) continue; const sv = source[key]; const tv = out[key]; out[key] = isPlainObject(tv) && isPlainObject(sv) ? deepMerge(tv, sv) : deepCloneExtended(sv); } return out; }

null replaces. Feature flags and theme config use this.

Follow-ups

Q. Why skip those keys?

Answer: Prototype pollution from untrusted config.

Problem 4 — deepEqual

Question: Implement deepEqual(a, b).

Description: Return whether a and b have the same structure and values. Use Object.is so NaN equals NaN and identity is exact. A key whose value is undefined is not the same as a missing key. Cycles must not recurse forever — store pairs (left object → right object), not a boolean “visited” flag.

function deepEqual(a, b) {}

Example

deepEqual({ a: 1, b: [2] }, { a: 1, b: [2] }); // true deepEqual(NaN, NaN); // true deepEqual({ a: undefined }, {}); // false

Solution

function deepEqual(a, b, seen = new WeakMap()) { if (Object.is(a, b)) { return true; } if (typeof a !== "object" || typeof b !== "object" || !a || !b) { return false; } if (seen.has(a)) { return seen.get(a) === b; } seen.set(a, b); const keysA = Object.keys(a); if (keysA.length !== Object.keys(b).length) { return false; } return keysA.every( (key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key], seen) ); }

Use case

Tests and config comparison. Not every React render.

Follow-ups

Q. React?

Answer: Prefer Object.is / same reference. Deep equal is O(n) on the hot path.

Problem 5 — Equal Date, Map, Set

Question: Extend deepEqual for Date, RegExp, Map, and Set.

Description: Object.keys on a Map is empty, so property comparison is not enough. Same runtime type first. Date compares timestamps. RegExp compares source and flags. Map uses key identity (has / get), then deep-equal values. Set order does not matter; matching object elements is worst-case quadratic. Two different objects with the same shape are different Map keys unless you add structural key matching.

function deepEqualExtended(a, b) {}

Example

deepEqualExtended(new Date("2024-01-01"), new Date("2024-01-01")); // true deepEqualExtended(new Set([1, 2]), new Set([2, 1])); // true

Solution

function deepEqualExtended(a, b, seen = new WeakMap()) { if (Object.is(a, b)) return true; if (typeof a !== "object" || typeof b !== "object" || !a || !b) { return false; } if (tag(a) !== tag(b)) return false; if (seen.has(a)) { return seen.get(a) === b; } seen.set(a, b); switch (tag(a)) { case "[object Date]": return Object.is(a.getTime(), b.getTime()); case "[object RegExp]": return a.source === b.source && a.flags === b.flags; case "[object Map]": { if (a.size !== b.size) return false; for (const [k, v] of a) { if (!b.has(k) || !deepEqualExtended(v, b.get(k), seen)) { return false; } } return true; } case "[object Set]": { if (a.size !== b.size) return false; for (const v of a) { let found = false; for (const w of b) { if (deepEqualExtended(v, w, seen)) { found = true; break; } } if (!found) return false; } return true; } case "[object Array]": if (a.length !== b.length) return false; return a.every((item, i) => deepEqualExtended(item, b[i], seen)); default: { const keysA = Object.keys(a); if (keysA.length !== Object.keys(b).length) return false; return keysA.every( (key) => Object.hasOwn(b, key) && deepEqualExtended(a[key], b[key], seen) ); } } }

Complexity

PieceTimeSpace
Clone / merge / object equalO(n)O(n)
Set of objectsO(n²) worstO(n)

Senior Engineer Interview Answer

“Clone is graph copy: register the empty clone in a WeakMap before walking children. Built-ins are not enumerable bags. Merge only recurses when both sides are plain objects; arrays replace. Equal starts with Object.is, pairs cyclic nodes, and does not treat missing keys as undefined. In React I keep identity instead of deep-comparing on render.”

Interview Checklist

✓ seen.set before recurse ✓ cycles ✓ Date / Map / Set / RegExp ✓ Map forEach (value, key) ✓ merge ≠ spread ✓ Object.is + NaN ✓ missing vs undefined ✓ structuredClone vs JSON

References first. Recursion second.

Last updated on