InterviewsVector
advancedVery common6 min read · Updated Jul 18, 2026

Implement Deep Clone (Cycles, Dates, Maps, and the Honest Limits)

Implement deepClone in JavaScript with WeakMap cycle handling, Date/RegExp/Map/Set support, prototype preservation — and when to just use structuredClone.


The problem

Implement deepClone(value): a fully independent copy — mutate the clone anywhere, the original never notices. Handle nested objects/arrays, Date, Map, Set, and circular references.

This question is a recursion-plus-type-dispatch drill with one famous trap (cycles) and one senior-level kicker: knowing exactly where your clone lies to you — because every deep clone, including the platform's, has a boundary, and naming yours is worth more than handling one more type.

Implementation

function deepClone(value, visited = new WeakMap()) {
  // Primitives (and functions — see the limits section) pass through
  if (value === null || typeof value !== "object") {
    return value;
  }
 
  // Cycles & shared references: if we've cloned this object on this walk,
  // return THAT clone — same trick as stringify's ancestor set, but as a
  // mapping so shared subtrees stay shared instead of duplicating.
  if (visited.has(value)) {
    return visited.get(value);
  }
 
  if (value instanceof Date) return new Date(value.getTime());
  if (value instanceof RegExp) return new RegExp(value.source, value.flags);
 
  if (value instanceof Map) {
    const clone = new Map();
    visited.set(value, clone);
    for (const [k, v] of value) {
      clone.set(deepClone(k, visited), deepClone(v, visited)); // keys clone too
    }
    return clone;
  }
 
  if (value instanceof Set) {
    const clone = new Set();
    visited.set(value, clone);
    for (const v of value) clone.add(deepClone(v, visited));
    return clone;
  }
 
  if (Array.isArray(value)) {
    const clone = [];
    visited.set(value, clone);
    value.forEach((item, i) => { clone[i] = deepClone(item, visited); });
    return clone;
  }
 
  // Plain-ish objects: keep the prototype so class instances stay instances
  const clone = Object.create(Object.getPrototypeOf(value));
  visited.set(value, clone);
  for (const key of Reflect.ownKeys(value)) { // string AND symbol keys
    clone[key] = deepClone(value[key], visited);
  }
  return clone;
}

The order of operations is the algorithm

Register the clone in visited before recursing into children. That's the entire cycle solution: when the walk comes back around to a self-reference, the half-built clone is already in the map and gets returned, wiring the cycle into the copy exactly as it was in the original:

const original = { name: "root" };
original.self = original;
 
const clone = deepClone(original);
clone.self === clone;      // true — the cycle points at the CLONE
clone.self === original;   // false — no back-door to the original
 
const shared = { hits: 0 };
const doc = { a: shared, b: shared };
const copy = deepClone(doc);
copy.a === copy.b;         // true — shared stays shared (a WeakMap *mapping*
                           // buys this; a visited *set* that just throws wouldn't)

The WeakMap (vs Map) choice is one sentence: keys are objects, held weakly, so the bookkeeping can't extend any object's lifetime — the same reasoning as WeakMap-keyed memoization.

The honest limits (the senior section)

Every deep clone has a boundary. Yours:

  • Functions pass through by reference. A function's captured closure scope cannot be cloned from userland — a "copied" function sharing the original's state would be a lie, so sharing the reference is the least dishonest option. (structuredClone refuses outright and throws.)
  • Getters become snapshotsclone[key] = value[key] evaluates accessors (the same Get/Set semantics as Object.assign); cloning descriptors verbatim needs getOwnPropertyDescriptors and a decision about whether a live getter on a clone even makes sense.
  • Platform objects (DOM nodes, File, streams) — instanceof-dispatch can't reconstruct engine-internal state; DOM nodes have their own cloneNode.
  • Class instances survive shape but not privates — the Object.create(getPrototypeOf(...)) line keeps methods working, but #private fields are installed only by the constructor and are invisible to reflection: the clone has the prototype, not the private state. Very few candidates know this; it's a great volunteered detail.

deepClone vs the alternatives

JSON.parse(JSON.stringify(x))structuredClone(x)this implementation
Cyclesthrows
Date→ string ❌
Map/Set{}
undefined/functions in objectssilently droppedfunctions throwkept / shared by ref
Prototypes / class instanceslostlostpreserved
Gettersevaluatedevaluatedevaluated

The modern default is structuredClone — but note the row it loses: it does not preserve prototypes (a class Point instance comes back a plain object). "I'd reach for structuredClone unless class instances matter, and here's the from-scratch version because interviews and prototype preservation both exist" is the complete answer.

Edge cases interviewers probe

  • Cycles and shared references — the two distinct cases above; interviewers test shared references specifically to catch throw-on-revisit implementations (the same false-circular bug as in stringify).
  • Symbol keys and non-enumerablesReflect.ownKeys picks up symbols; full descriptor fidelity is the getOwnPropertyDescriptors extension — know the API name even if you don't write it.
  • Sparse arraysforEach skips holes, so holes stay holes (matching reduce's hole semantics); an index for loop would densify them with undefined.
  • Deep nesting — recursion depth = structure depth; the explicit-stack rewrite is the same escalation as in JSON.parse.
  • Map keys — cloning values but not keys is the common half-measure; whether object keys should be cloned (breaking identity-keyed lookups!) is a genuinely good design argument to have out loud.

Common mistakes

  • Registering in visited after recursing — infinite loop on the first cycle.
  • No Date/RegExp branches — they fall into the plain-object path and come out as {} with the wrong prototype.
  • Object.keys + {} — silently strips prototypes and symbol keys without being able to say so (doing it knowingly as a "plain data only" contract is fine — the crime is not knowing).
  • Presenting JSON.parse(JSON.stringify(...)) as deep clone without its failure table.
  • Claiming functions are cloned.

Follow-up questions

  • "Why does a visited-set-that-throws differ from your WeakMap?" — set detects revisits (rejects DAGs); mapping detects revisits and reuses the clone (preserves DAGs and cycles). Same distinction as stringify vs clone semantics.
  • "Deep clone with a depth limit / key filter?" — thread depth/shouldClone(key) through the recursion — turns the drill into an API-design conversation.
  • "How does structuredClone handle what you can't?" — it runs in the engine with serialization semantics (same algorithm as postMessage); the full comparison.
  • "Clone vs immutable updates?" — cloning everything to change one field is O(n) worship; structural sharing (spread the path, share the rest — the Immer idea) is why Redux never needed deep clone.
  • "Test it?" — clone, mutate every level of the clone, assert the original byte-for-byte unchanged (deep equal — the sibling question — is literally the assertion tool).

  • Custom Deep Equal

    intermediate

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

  • When to use the built-in structuredClone, where it fails (functions, prototypes), and how it compares to JSON round-tripping.

  • Deep Flatten

    beginner

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

  • Turn nested objects into dot-path keys ({ 'a.b.c': 1 }) and back — the transform behind form libraries and analytics events.