Deep Clone with Circular References
Key Learning Objectives
Differentiate shallow copying (Object.assign, spread) from true recursive deep cloning.
Resolve circular object references using a WeakMap memoization cache to prevent infinite recursion and stack overflow.
Correctly clone specialized built-in objects: Date (via getTime), RegExp (source, flags), Map, and Set.
Preserve object prototype inheritance chains using Object.create(Object.getPrototypeOf(value)).
Analyze limitations of JSON serialization (JSON.parse(JSON.stringify)) and structuredClone vs custom recursion.
The Interview Problem
What is logged to the console when the following deepClone implementation executes on an object containing circular references, nested state, Date, and Set?
1function deepClone(value, visited = new WeakMap()) {2 if (value === null || typeof value !== 'object') {3 return value;4 }5 if (visited.has(value)) {6 return visited.get(value);7 }8 if (value instanceof Date) {9 return new Date(value.getTime());10 }11 if (value instanceof RegExp) {12 return new RegExp(value.source, value.flags);13 }14 if (value instanceof Set) {15 const cloneSet = new Set();16 visited.set(value, cloneSet);17 value.forEach((v) => cloneSet.add(deepClone(v, visited)));18 return cloneSet;19 }20 if (value instanceof Map) {21 const cloneMap = new Map();22 visited.set(value, cloneMap);23 value.forEach((v, k) => cloneMap.set(deepClone(k, visited), deepClone(v, visited)));24 return cloneMap;25 }26 const result = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));27 visited.set(value, result);28 for (const key of Object.keys(value)) {29 result[key] = deepClone(value[key], visited);30 }31 return result;32}3334const original = {35 num: 42,36 date: new Date('2026-01-01T00:00:00Z'),37 tags: new Set(['js', 'react']),38 nested: { count: 1 }39};40original.self = original;4142const cloned = deepClone(original);4344const res = [45 original === cloned,46 cloned.self === cloned,47 original.nested === cloned.nested,48 cloned.date.getTime() === original.date.getTime(),49 original.date !== cloned.date,50 cloned.tags.has('js')51].join(' ');5253console.log(res);
Predict Console Output
Select the option that matches what standard ECMAScript prints to the console:
false true false true true true
false false false true true true
false true true true false true
RangeError: Maximum call stack size exceeded
V8 Engine Execution Trace
Step 1 of 6 (Line 38)deepClone is called with original. It allocates a new object inheriting from original's prototype and caches [original -> result] in the WeakMap.
Deep Technical Breakdown
Deep Cloning: The Core Problem
A shallow copy (Object.assign({}, obj) or { ...obj }) only copies top-level properties. Any nested objects or arrays remain shared memory references.
Why JSON.parse(JSON.stringify(obj)) is Insufficient
In interview and production scenarios, JSON.stringify breaks on common JavaScript types:
- Circular References: Throws
TypeError: Converting circular structure to JSON. - Date Instances: Coerced to ISO strings; does not restore
Dateinstances on parse. - Map & Set: Serialized to empty objects
{}. - undefined, Symbols, Functions: Silently dropped from objects or converted to
nullin arrays. - NaN & Infinity: Converted to
null.
The WeakMap Solution for Circular Graphs
To handle circular references (e.g., A -> B -> A or self-referential obj.self = obj), we pass a visited = new WeakMap() across recursive frames:
- If
visited.has(value), return the cached clone (visited.get(value)). - Allocate the clone container immediately.
- Register
visited.set(value, clone)before iterating over child properties. - Recurse over children. If a child references an ancestor, step 1 returns the already-created clone reference, successfully resolving the cycle without stack overflow.
Limitations of Pure JS Deep Cloning
Even robust custom deep clone implementations have clear boundaries:
- Functions: Generally shared by reference rather than cloned, because re-binding closures is impossible in user-space JS.
- DOM Elements: Contain native browser pointers; cloning requires
node.cloneNode(true). - Symbols & Descriptors: Requires
Reflect.ownKeys()andObject.getOwnPropertyDescriptors()to preserve non-enumerable properties and symbol keys.
Common Traps & Mistakes
Placing `visited.set(value, result)` after the recursive child loop instead of before, causing infinite recursion on self-referential properties.
Using a regular `Map` or `Array` for visited tracking, which permanently retains object references in memory and creates memory leaks.
Failing to handle `null`: `typeof null === 'object'`, which causes `null` to reach object cloning logic if not checked first.
Cloning dates by reference (`result.date = value.date`) instead of instantiating `new Date(value.getTime())`.
FAANG Follow-Up Probes
Probe #1
How does the native `structuredClone()` API compare to custom recursive cloning in terms of supported types and performance?
Probe #2
How would you extend this implementation to copy non-enumerable properties and Symbol keys using `Reflect.ownKeys()`?
Probe #3
Why does `structuredClone()` throw a DOMException when encountering functions, and how should a production utility handle them?
