Implement Deep Clone and Deep Equal
Why These Two Functions Keep Showing Up
Deep clone and deep equal are interview staples at Google, Meta, and Amazon — not because they're exotic, but because they're deceptively simple. Everyone can write the naive version. Almost nobody handles all the edge cases correctly on the first pass.
These two functions also teach you a ton about how JavaScript actually works: reference vs value semantics, prototype chains, property descriptors, and the surprisingly long list of built-in object types you need to handle differently.
We'll build both from scratch, progressively. Start simple, break it, fix it, repeat — until we have something production-worthy.
Think of deep clone as photocopying a family tree. A shallow copy only copies the first page — it still points to the same children. A deep copy duplicates every page, every child, every grandchild. But what happens when two family members are married (circular reference)? You need to remember who you've already copied, or you'll photocopying forever.
Part 1: Deep Clone
The Naive Approach (and Why It Breaks)
Most developers reach for this first:
function naiveClone(obj) {
return JSON.parse(JSON.stringify(obj));
}
It works for plain objects and arrays. But it silently destroys data:
const original = {
date: new Date("2025-01-01"),
pattern: /hello/gi,
fn: () => "hi",
undef: undefined,
inf: Infinity,
nan: NaN,
map: new Map([["a", 1]]),
set: new Set([1, 2, 3]),
};
const cloned = JSON.parse(JSON.stringify(original));
// date → "2025-01-01T00:00:00.000Z" (string, not Date)
// pattern → {} (empty object)
// fn → gone (removed entirely)
// undef → gone (removed entirely)
// inf → null (coerced)
// nan → null (coerced)
// map → {} (empty object)
// set → {} (empty object)
And if the object has a circular reference, it throws:
const obj = { name: "loop" };
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // TypeError: Converting circular structure to JSON
Building It Up: Handle Primitives and Plain Objects
Let's start with the core recursive structure:
function deepClone(value) {
if (value === null || typeof value !== "object") {
return value;
}
if (Array.isArray(value)) {
return value.map((item) => deepClone(item));
}
const result = {};
for (const key of Object.keys(value)) {
result[key] = deepClone(value[key]);
}
return result;
}
This handles primitives, plain objects, and arrays. But it still breaks on Date, RegExp, Map, Set, and circular references.
Handling Exotic Built-in Types
Date, RegExp, Map, and Set are all objects, but they carry internal state that a simple property copy misses. You need to construct new instances:
function deepClone(value) {
if (value === null || typeof value !== "object") {
return 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 result = new Map();
for (const [k, v] of value) {
result.set(deepClone(k), deepClone(v));
}
return result;
}
if (value instanceof Set) {
const result = new Set();
for (const item of value) {
result.add(deepClone(item));
}
return result;
}
if (Array.isArray(value)) {
return value.map((item) => deepClone(item));
}
const result = {};
for (const key of Object.keys(value)) {
result[key] = deepClone(value[key]);
}
return result;
}
Better. But we're still vulnerable to infinite recursion with circular references.
Defeating Circular References with WeakMap
This is where it gets interesting. A circular reference means an object eventually refers back to itself (directly or through a chain). If we don't track what we've already cloned, we recurse forever and blow the stack.
The solution: use a WeakMap to remember every object we've already cloned. Before cloning an object, check if we've seen it. If yes, return the existing clone. If no, create the clone, store it in the map, then populate its properties.
The key insight is that we store the clone in the map before we recurse into its children. This way, when a child eventually points back to the parent, the map already has the clone ready.
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) {
return new Date(value.getTime());
}
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
if (value instanceof Map) {
const result = new Map();
seen.set(value, result);
for (const [k, v] of value) {
result.set(deepClone(k, seen), deepClone(v, seen));
}
return result;
}
if (value instanceof Set) {
const result = new Set();
seen.set(value, result);
for (const item of value) {
result.add(deepClone(item, seen));
}
return result;
}
if (Array.isArray(value)) {
const result = [];
seen.set(value, result);
for (const item of value) {
result.push(deepClone(item, seen));
}
return result;
}
const result = Object.create(Object.getPrototypeOf(value));
seen.set(value, result);
for (const key of Object.keys(value)) {
result[key] = deepClone(value[key], seen);
}
return result;
}
Notice that for the plain object case we use Object.create(Object.getPrototypeOf(value)) instead of {}. This preserves the prototype chain of the original object. Using {} would always create an object with Object.prototype, losing any custom prototype. In an interview, this detail separates "solid" from "exceptional."
Why WeakMap specifically, not a regular Map? Because WeakMap holds weak references to its keys. Once the original objects are no longer referenced elsewhere, they can be garbage collected. A regular Map would keep them alive, creating a memory leak if deepClone is called repeatedly.
The Production Version
Let's put it all together with one more refinement — handling symbol keys:
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) return new Date(value.getTime());
if (value instanceof RegExp) return new RegExp(value.source, value.flags);
if (value instanceof Map) {
const clone = new Map();
seen.set(value, clone);
for (const [k, v] of value) {
clone.set(deepClone(k, seen), deepClone(v, seen));
}
return clone;
}
if (value instanceof Set) {
const clone = new Set();
seen.set(value, clone);
for (const v of value) clone.add(deepClone(v, seen));
return clone;
}
const clone = Array.isArray(value)
? []
: Object.create(Object.getPrototypeOf(value));
seen.set(value, clone);
for (const key of Reflect.ownKeys(value)) {
clone[key] = deepClone(value[key], seen);
}
return clone;
}
Reflect.ownKeys(value) returns all own property keys — strings and symbols — in a single call. This is cleaner than combining Object.keys() with Object.getOwnPropertySymbols().
What About structuredClone?
Since 2022, browsers and Node.js provide structuredClone() — a native deep clone built on the HTML structured clone algorithm. It handles:
- Circular references
- Date, RegExp, Map, Set, ArrayBuffer, Blob, File, ImageData
- Transferable objects (with second argument)
But it has limitations:
const obj = {
fn: () => "hello",
dom: document.createElement("div"),
err: new Error("oops"),
};
structuredClone(obj);
// Throws: () => "hello" could not be cloned (functions)
// Throws: HTMLDivElement could not be cloned (DOM nodes)
// Error works — but the stack trace is lost
structuredClone vs manual deepClone — when to use which
Use structuredClone when you're cloning data — plain objects, arrays, Maps, Sets, Dates, typed arrays. It's native, fast, and handles circular refs automatically.
Use a manual deepClone when:
- You need to clone objects with functions (event handlers, callbacks)
- You need to preserve custom prototypes precisely
- You need to handle DOM nodes
- You're in an interview and they ask you to implement it from scratch
In production code, structuredClone should be your default. But understanding how to build one from scratch is what separates someone who uses tools from someone who understands them.
Part 2: Deep Equal
Deep equality is the mirror image of deep cloning. Instead of creating a copy, you're verifying that two values are structurally identical. Same challenge: you need to handle every type differently, deal with circular references, and navigate JavaScript's quirky equality semantics.
Start Simple, Then Break It
function naiveEqual(a, b) {
return JSON.stringify(a) === JSON.stringify(b);
}
Same problems as the JSON clone approach, plus a new one: property order matters in JSON strings but shouldn't matter for equality:
JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 });
// false — different key order, different strings
// But these objects are structurally equal
The Edge Cases That Matter
Before writing the real implementation, let's catalog the edge cases that trip up candidates:
NaN is not equal to itself (but should be in deep equal):
NaN === NaN; // false — IEEE 754 spec
// But deepEqual({ x: NaN }, { x: NaN }) should return true
+0 and -0 are equal with === (but are semantically different):
+0 === -0; // true
Object.is(+0, -0); // false
1 / +0; // Infinity
1 / -0; // -Infinity
Whether your deepEqual treats +0 and -0 as equal is a design choice. Object.is says they're different. Most deep equal implementations (including Node's assert.deepStrictEqual) treat them as equal — following === semantics for numbers, except for NaN. We'll do the same, but you should know the tradeoff.
Dates compare by time value, not reference:
new Date("2025-01-01") === new Date("2025-01-01"); // false — different refs
// deepEqual should compare .getTime() values
RegExp compare by source and flags:
/abc/gi === /abc/gi; // false — different refs
// deepEqual should compare .source and .flags
Map and Set compare by contents, not reference:
new Map([["a", 1]]) === new Map([["a", 1]]); // false
// deepEqual should iterate and compare entries
Null prototype objects:
const obj = Object.create(null);
obj.name = "no prototype";
// obj has no .toString(), .hasOwnProperty(), etc.
// Your code must not call methods it assumes exist
Building Deep Equal
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (
a === null || b === null ||
typeof a !== "object" || typeof b !== "object"
) {
return false;
}
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
return false;
}
if (seen.has(a)) {
return seen.get(a) === b;
}
seen.set(a, b);
if (a instanceof Date) {
return a.getTime() === b.getTime();
}
if (a instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, val] of a) {
if (!b.has(key) || !deepEqual(val, b.get(key), seen)) {
return false;
}
}
return true;
}
if (a instanceof Set) {
if (a.size !== b.size) return false;
for (const val of a) {
if (!b.has(val)) return false;
}
return true;
}
const keysA = Reflect.ownKeys(a);
const keysB = Reflect.ownKeys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!deepEqual(a[key], b[key], seen)) {
return false;
}
}
return true;
}
Let's walk through the design decisions:
Object.is(a, b) as the first check. This handles primitives, same-reference objects, and the NaN case. Object.is(NaN, NaN) returns true, which is exactly what we want. It also returns false for Object.is(+0, -0), but since we check typeof !== "object" after this, +0 and -0 are handled by the Object.is line and would return false. If you want +0 === -0 to be true, you'd add a special case for numbers.
Prototype check. { a: 1 } and an instance of a class with { a: 1 } should not be equal — they're structurally different types. Comparing prototypes catches this.
Circular reference handling with WeakMap. Same idea as deep clone. We map a to b, and if we encounter a again, we check if the previously paired b is the same b we're comparing against now.
Object.prototype.hasOwnProperty.call(b, key). We use call instead of b.hasOwnProperty(key) because b might have a null prototype (Object.create(null)), which means hasOwnProperty doesn't exist on it directly.
Reflect.ownKeys. Same as in deep clone — catches both string and symbol keys.
The Set Equality Trap
Look at the Set comparison above carefully. It works for primitive values, but what about Sets containing objects?
const a = new Set([{ x: 1 }]);
const b = new Set([{ x: 1 }]);
deepEqual(a, b); // false — because b.has({ x: 1 }) uses reference equality
Set.has() uses SameValueZero — essentially === — so it compares object references, not deep equality. To handle Sets with object values, you'd need an O(n^2) comparison:
if (a instanceof Set) {
if (a.size !== b.size) return false;
for (const valA of a) {
let found = false;
for (const valB of b) {
if (deepEqual(valA, valB, seen)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
This is O(n^2) and there's no good way around it for Sets with non-primitive values. In an interview, mention this tradeoff. Most production implementations (like Lodash's isEqual) do handle it.
The same problem applies to Maps with object keys. Map.has(key) uses reference equality for keys. If both Maps have the key { id: 1 } but they're different object references, b.has(key) returns false. For truly robust Map comparison, you'd iterate both Maps and do deep-equal matching on keys too — another O(n^2) scenario. In interviews, acknowledging this shows you understand the full problem space.
The Final Implementation with Set Fix
function deepEqual(a, b, seen = new WeakMap()) {
if (Object.is(a, b)) return true;
if (
a === null || b === null ||
typeof a !== "object" || typeof b !== "object"
) {
return false;
}
if (Object.getPrototypeOf(a) !== Object.getPrototypeOf(b)) {
return false;
}
if (seen.has(a)) {
return seen.get(a) === b;
}
seen.set(a, b);
if (a instanceof Date) {
return a.getTime() === b.getTime();
}
if (a instanceof RegExp) {
return a.source === b.source && a.flags === b.flags;
}
if (a instanceof Map) {
if (a.size !== b.size) return false;
for (const [key, val] of a) {
let matched = false;
for (const [keyB, valB] of b) {
if (deepEqual(key, keyB, seen) && deepEqual(val, valB, seen)) {
matched = true;
break;
}
}
if (!matched) return false;
}
return true;
}
if (a instanceof Set) {
if (a.size !== b.size) return false;
for (const valA of a) {
let found = false;
for (const valB of b) {
if (deepEqual(valA, valB, seen)) {
found = true;
break;
}
}
if (!found) return false;
}
return true;
}
const keysA = Reflect.ownKeys(a);
const keysB = Reflect.ownKeys(b);
if (keysA.length !== keysB.length) return false;
for (const key of keysA) {
if (!Object.prototype.hasOwnProperty.call(b, key)) {
return false;
}
if (!deepEqual(a[key], b[key], seen)) {
return false;
}
}
return true;
}
The Interview Cheat Sheet
When you're asked to implement either function in an interview, here's the progression that shows mastery:
| What developers do | What they should do |
|---|---|
| Using JSON.parse(JSON.stringify(obj)) as a deep clone JSON round-trip silently destroys Date, RegExp, Map, Set, undefined, NaN, Infinity, functions, and throws on circular references | Build a proper recursive clone or use structuredClone for data objects |
| Forgetting to store the clone in WeakMap before recursing If you populate properties first, a circular back-reference will recurse infinitely before the WeakMap entry exists | Always seen.set(value, clone) immediately after creating the clone, before populating its properties |
| Using b.hasOwnProperty(key) directly in deepEqual Objects with null prototypes (Object.create(null)) don't have hasOwnProperty, and the method can also be shadowed by an own property | Use Object.prototype.hasOwnProperty.call(b, key) |
| Comparing Sets with Set.has() for deep equality Set.has uses reference equality (SameValueZero), so two different objects with the same structure won't match | Use O(n^2) iteration with deep comparison for Sets containing objects |
| Using === to check NaN equality in deepEqual NaN !== NaN by the IEEE 754 spec, but deep equality should consider two NaN values as equal | Use Object.is(a, b) which returns true for NaN === NaN |
| Creating the clone with {} instead of Object.create(Object.getPrototypeOf(value)) Using {} always creates an object with Object.prototype, losing any custom prototype from the original | Preserve the original prototype chain using Object.create(Object.getPrototypeOf(value)) |
- 1Always handle primitives first — null check, then typeof !== object as the base case
- 2Use WeakMap for circular reference tracking — store the clone/pair BEFORE recursing into children
- 3Handle exotic built-ins explicitly — Date (getTime), RegExp (source + flags), Map (iterate), Set (iterate)
- 4Use Object.is() for NaN-safe equality, Object.getPrototypeOf() for prototype-aware cloning
- 5Use Reflect.ownKeys() to catch both string and symbol property keys
- 6Use Object.prototype.hasOwnProperty.call() — never call hasOwnProperty directly on the target object
- 7In interviews, build progressively: primitives → objects → built-ins → circular refs → edge cases