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;
// 1obj.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;
// 1Solution
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 }, {});
// falseSolution
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]));
// trueSolution
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
| Piece | Time | Space |
|---|---|---|
| Clone / merge / object equal | O(n) | O(n) |
| Set of objects | O(n²) worst | O(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 JSONReferences first. Recursion second.