Deep Clone Complex Objects (Dates, Functions, Circular References)

Advanced16 min interview
Skills tested:
Writing a recursive deepClone with WeakMap-based cycle detectionHandling special types: Date, RegExp, Map, Set, Array separately from plain objectsWhy structuredClone is preferred over JSON.parse/stringify or manual recursion in modern codeWhat structuredClone cannot handle: functions, class instances with methods, DOM nodesWhen to avoid deep cloning entirely in favor of structural sharing or immutable patterns

Advertisement

🧩 Scenario

In a real codebase, deep cloning is needed when you must produce a completely independent copy of a complex nested object before mutating it, such as when implementing undo/redo history, when serializing application state, or when passing state to a background Web Worker. The risk is cloning objects that contain circular references (parent-child DOM-like trees, linked lists, graph nodes), which will crash a naive recursive implementation.

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

MethodHandles Dates/MapsHandles functionsHandles circular refsPerformance
JSON.parse/JSON.stringifyNo (Date to string)No (dropped)No (throws)Fast for simple data
structuredCloneYesNo (dropped)YesFast (native)
Custom recursive + WeakMapConfigurableCan be addedYes (WeakMap)Slower, flexible
lodash.cloneDeepYesYes (by ref)YesModerate (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 structuredClone exists and when to use it vs a custom implementation
  • Whether you understand why deep cloning class instances loses prototype methods

Follow-Up Questions

  1. How would you extend the recursive clone to preserve class prototype chains so instances remain instances?
  2. When would you prefer structural sharing (like Immer.js) over deep cloning for state updates?
  3. How does Reflect.ownKeys differ from Object.keys and why is it used in a thorough deep clone?
  4. If you deep clone a 100MB nested structure, what performance and memory concerns arise?
  5. How does the structured clone algorithm used by postMessage differ from structuredClone()?

Common Candidate Mistakes

  • Using JSON.parse(JSON.stringify(obj)) and not knowing that Date becomes a string and functions disappear
  • Writing a recursive clone without a seen map 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 structuredClone is a built-in and reaching for lodash.cloneDeep unnecessarily in modern code
  • Not handling Date, RegExp, Map, and Set as 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/stringify fails 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 structuredClone handles and what it does not?
  • Can you explain what the WeakMap tracks and why it is better than a regular Map here?
  • 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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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