intermediate

structuredClone vs JSON.parse vs Custom Deep Clone in JavaScript

Compare structuredClone, JSON.parse(JSON.stringify()), and hand-written deep clone — what each handles, where each fails, and which to choose in interviews.


The interview setup

"Deep clone this object" used to mean writing recursion. Today the strongest answer starts differently: "Can I use structuredClone?" — then demonstrates you know exactly what it does and where it fails. This page compares the three approaches so you can pick deliberately and defend the choice.

Option 1: structuredClone (the modern default)

const original = {
  date: new Date(),
  pattern: /abc/gi,
  buffer: new Map([["a", new Set([1, 2])]]),
  nested: { deep: [1, [2, [3]]] },
};
original.self = original;                 // circular!
 
const copy = structuredClone(original);
 
copy.self === copy;                        // true — cycle preserved
copy.date instanceof Date;                 // true
copy.buffer.get("a") instanceof Set;       // true

structuredClone uses the same structured-clone algorithm as postMessage/IndexedDB. It handles what JSON never could:

  • Circular references — preserved, no crash.
  • Built-in typesDate, RegExp, Map, Set, ArrayBuffer, typed arrays, Blob, File, Error.
  • Identity sharing — if two properties reference the same object, the clone shares one copy too.

Where structuredClone fails (the senior part)

structuredClone({ fn: () => {} });     // ❌ DataCloneError — functions
structuredClone({ el: document.body }); // ❌ DataCloneError — DOM nodes
structuredClone(new (class X {})());    // ⚠️ becomes plain object — prototype lost
const o = { get computed() { return 42; } };
structuredClone(o).computed;            // 42 — but now a plain data property

Four limits to recite: no functions, no DOM nodes, prototypes are dropped (class instances flatten to plain objects), and getters are evaluated, not copied. Symbols and property descriptors are also lost. If state contains methods or class instances, structuredClone silently isn't enough.

Option 2: JSON.parse(JSON.stringify(obj)) — know why it's weak

The old one-liner is now mostly an interview trap:

const copy = JSON.parse(JSON.stringify(original));

Failure modes (know them cold — enumerated in depth in custom JSON.stringify):

  • Circular references throw (TypeError: Converting circular structure).
  • undefined, functions, and symbols are dropped (or nulled in arrays).
  • Date becomes a string; Map/Set become {}; NaN/Infinity become null.
  • Getters are evaluated; prototypes are lost.
  • It round-trips through a string — slow for large graphs.

Acceptable only for known-JSON-safe data (API payloads, config). If you offer it, say those caveats in the same breath.

Option 3: hand-written deep clone (the exercise itself)

When the interviewer says "no built-ins," you're being tested on recursion, type dispatch, and cycle handling — the full implementation with a WeakMap lives in custom deep clone. The skeleton:

function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== "object") return value; // primitives + functions
  if (seen.has(value)) return seen.get(value);                   // cycles
 
  let result;
  if (value instanceof Date) result = new Date(value);
  else if (value instanceof RegExp) result = new RegExp(value.source, value.flags);
  else if (value instanceof Map) {
    result = new Map();
    seen.set(value, result);
    for (const [k, v] of value) result.set(deepClone(k, seen), deepClone(v, seen));
    return result;
  } else if (value instanceof Set) {
    result = new Set();
    seen.set(value, result);
    for (const v of value) result.add(deepClone(v, seen));
    return result;
  } else if (Array.isArray(value)) {
    result = [];
    seen.set(value, result);
    value.forEach((v, i) => (result[i] = deepClone(v, seen)));
    return result;
  } else {
    result = Object.create(Object.getPrototypeOf(value)); // preserve the prototype!
    seen.set(value, result);
    for (const key of Reflect.ownKeys(value)) {
      result[key] = deepClone(value[key], seen);
    }
    return result;
  }
  return result;
}

Note the two things a custom clone can do that structuredClone can't: preserve prototypes (Object.create(Object.getPrototypeOf(value)) — see prototypal inheritance) and decide policy for functions (share the reference rather than throw).

Decision table

RequirementstructuredCloneJSON round-tripCustom clone
Circular references❌ throws✅ with WeakMap
Date / Map / Set / RegExp❌ corrupted✅ if you code it
Functions in the graph❌ throws❌ dropped✅ your policy
Class instances (prototype)❌ flattened❌ flattened✅ preserved
Performance on big graphsFast (native)Slow (string)Depends
AvailabilityAll modern runtimesEverywhereEverywhere

The answer that gets hired

"Default to structuredClone — native, handles cycles and built-ins. If the object holds functions or class instances, I need a custom clone with a WeakMap for cycles and prototype preservation — here's how I'd write it. JSON round-trip only for known JSON-safe data, and here's everything it silently breaks."

Three tools, trade-offs stated, then implement the one they ask for. Follow-ups worth anticipating: shallow vs deep ({ ...obj } copies one level; nested objects are shared references) and cloning across realms (structured clone is also the algorithm used to send data to workers via postMessage).


  • Recursively clone nested objects and arrays, handling cycles with a WeakMap.

  • Custom Deep Equal

    intermediate

    Structural equality for nested data: type checks, key comparison, and recursion done right.

  • Deep Flatten

    beginner

    Flatten arbitrarily nested arrays recursively and iteratively — then compare with Array.prototype.flat.

  • Use a Proxy to support arr[-1] like Python — a practical introduction to Proxy traps.