Deep Clone Complex Objects (Dates, Functions, Circular References)
Advertisement
🧩 Scenario
Architecture Walkthrough
Why Naive Approaches Fail
JSON.parse(JSON.stringify(obj)) fails for five important types: Date is converted to its ISO string representation, functions and undefined are silently dropped from the output, BigInt throws a serialization error, and circular references throw TypeError: Converting circular structure to JSON. Map, Set, and RegExp are also lost (they serialize to {} or a string form, not a copy).
A naive recursive function that calls itself for every nested property will enter an infinite loop when it encounters a circular reference, because a.child.parent === a means the function will keep descending without ever finding a leaf.
Recursive Clone with WeakMap Cycle Detection
The standard solution for a custom recursive clone is to pass a WeakMap through every recursive call. Before processing an object, check whether it already exists as a key in the WeakMap. If it does, the current path is a cycle: return the already-cloned copy from the WeakMap instead of recursing again. If it does not, create a new empty clone for this object, add it to the WeakMap immediately (before recursing into its properties), and then copy each property.
Adding the clone to the WeakMap before recursing is the critical ordering detail. If you add it after recursing, a circular reference would cause infinite recursion before the entry is ever added.
structuredClone: The Modern Default
structuredClone() (available in all modern browsers and Node.js 17+) handles Date, Map, Set, ArrayBuffer, TypedArray, RegExp, Boolean, Error, and circular references correctly. For the vast majority of application data, structuredClone is the right default: it is implemented natively, well-tested, and much faster than a recursive JavaScript function.
Its limitations are intentional: functions are not transferable and are silently omitted. Class instances lose their prototype (the clone is a plain object with the same data). DOM nodes throw. For these cases, either a custom recursive clone or a library like lodash.cloneDeep is needed.
Key Code Explained
function deepClone(obj, seen = new WeakMap()) {
// Primitives and null: return as-is (no clone needed)
if (obj === null || typeof obj !== 'object') return obj;
// Cycle detection: if this object was already seen, return its clone
if (seen.has(obj)) return seen.get(obj);
// Handle special types before generic object handling
if (obj instanceof Date) return new Date(obj.getTime());
if (obj instanceof RegExp) return new RegExp(obj.source, obj.flags);
if (obj instanceof Map) {
const clonedMap = new Map();
seen.set(obj, clonedMap); // register before recursing
obj.forEach((value, key) => clonedMap.set(deepClone(key, seen), deepClone(value, seen)));
return clonedMap;
}
if (obj instanceof Set) {
const clonedSet = new Set();
seen.set(obj, clonedSet);
obj.forEach((value) => clonedSet.add(deepClone(value, seen)));
return clonedSet;
}
// Array or plain object
const clone = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj));
seen.set(obj, clone); // CRITICAL: register before recursing into properties
for (const key of Reflect.ownKeys(obj)) {
clone[key] = deepClone(obj[key], seen);
}
return clone;
}
// Circular reference example: works correctly
const node = { value: 1 };
node.self = node; // circular reference
const cloned = deepClone(node);
console.log(cloned.value); // 1
console.log(cloned.self === cloned); // true — cycle preserved in clone
console.log(cloned !== node); // true — independent copy
// structuredClone: better default for most data
const data = {
name: 'Ghazi',
createdAt: new Date(),
scores: new Map([['js', 95], ['ts', 90]]),
nested: { tags: ['react', 'node'] },
};
const copy = structuredClone(data);
copy.nested.tags.push('prisma');
console.log(data.nested.tags); // ['react', 'node'] — independent
console.log(copy.createdAt instanceof Date); // true
// structuredClone limitation: functions are dropped
const withFn = { fn: () => 'hello', value: 42 };
const clonedFn = structuredClone(withFn);
console.log(clonedFn.fn); // undefined — function not transferred
console.log(clonedFn.value); // 42
The seen.set(obj, clone) call before the property loop is the cycle-detection mechanism. For a circular object like node.self = node, when the recursive call reaches node.self, it passes node to deepClone. The function checks seen.has(node), finds it (because it was registered before the property loop), and returns clone immediately instead of recursing again.
Tradeoffs
| Method | Handles Dates/Maps | Handles functions | Handles circular refs | Performance |
|---|---|---|---|---|
| JSON.parse/JSON.stringify | No (Date to string) | No (dropped) | No (throws) | Fast for simple data |
| structuredClone | Yes | No (dropped) | Yes | Fast (native) |
| Custom recursive + WeakMap | Configurable | Can be added | Yes (WeakMap) | Slower, flexible |
| lodash.cloneDeep | Yes | Yes (by ref) | Yes | Moderate (library) |
What Interviewers Actually Check
- Whether you know the specific failure cases of
JSON.parse/stringify - Whether you can explain the WeakMap cycle detection mechanism
- Whether you know to register the clone in the WeakMap before recursing into properties
- Whether you know
structuredCloneexists and when to use it vs a custom implementation - Whether you understand why deep cloning class instances loses prototype methods
Follow-Up Questions
- How would you extend the recursive clone to preserve class prototype chains so instances remain instances?
- When would you prefer structural sharing (like Immer.js) over deep cloning for state updates?
- How does
Reflect.ownKeysdiffer fromObject.keysand why is it used in a thorough deep clone? - If you deep clone a 100MB nested structure, what performance and memory concerns arise?
- How does the structured clone algorithm used by
postMessagediffer fromstructuredClone()?
Common Candidate Mistakes
- Using
JSON.parse(JSON.stringify(obj))and not knowing thatDatebecomes a string and functions disappear - Writing a recursive clone without a
seenmap and not knowing it will infinite-loop on circular references - Not knowing that
seen.set(obj, clone)must happen before recursing into properties, not after - Not knowing
structuredCloneis a built-in and reaching forlodash.cloneDeepunnecessarily in modern code - Not handling
Date,RegExp,Map, andSetas special cases, causing them to be cloned as plain objects
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain why
JSON.parse/stringifyfails for four specific types? - Can you describe the recursive deepClone structure with WeakMap cycle detection, including the ordering of
seen.set? - Can you explain what
structuredClonehandles and what it does not? - Can you explain what the WeakMap tracks and why it is better than a regular
Maphere? - Can you name a situation where deep cloning should be avoided?
Summary
Deep cloning an object means producing a completely independent copy where no nested reference is shared with the original. The common JSON.parse/stringify approach silently drops functions and undefined, converts Date to strings, loses Map and Set, and throws on circular references. It should only be used for simple plain data objects with no special types.
structuredClone() is the correct modern default. It handles Date, Map, Set, ArrayBuffer, RegExp, and circular references. Its limitation is functions: they are not cloneable and are silently omitted. Class instances are cloned as plain objects, losing their prototype methods.
For cases where structuredClone is insufficient (functions, prototype preservation, custom type handling), a recursive function with a WeakMap for cycle detection is the standard approach. The WeakMap registers each object before recursing into its properties, so any circular reference encountered deeper in the tree returns the already-cloned copy rather than triggering infinite recursion.
Why not use JSON.parse(JSON.stringify(obj))?
It fails for Dates (converts to string), functions (dropped), undefined (dropped), Maps, Sets, and throws on circular references.
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement