Deep Clone with Circular References

Hard100% Free~30 mins#deep-clone#circular-references#weakmap#recursion#object-copying#implementations
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}
33
34const 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;
41
42const cloned = deepClone(original);
43
44const 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(' ');
52
53console.log(res);
Predict Console Output
Interactive Challenge

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.

Call Stack (Top = Active)
Global Execution Context
deepClone(original)
Lexical Scope / Bindings
value:original
visited:WeakMap {}
Console Stream
> [empty]

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 Date instances on parse.
  • Map & Set: Serialized to empty objects {}.
  • undefined, Symbols, Functions: Silently dropped from objects or converted to null in 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:

  1. If visited.has(value), return the cached clone (visited.get(value)).
  2. Allocate the clone container immediately.
  3. Register visited.set(value, clone) before iterating over child properties.
  4. 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() and Object.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?