ESC

Type to search the knowledge base.

Deep Clone Implementation

Interview deep clone: JSON is a lossy subset, structuredClone is the host, and recursion plus a WeakMap is how you survive cycles.

intermediate6 min read
  • javascript
  • clone
  • interview
  • objects

JSON.parse(JSON.stringify(user)) looks like a deep clone until a Date becomes a string, a method disappears, undefined keys vanish, a Map turns into {}, and a parent pointer throws TypeError. The next candidate writes a recursive copy and hangs the tab on node.self = node.

There are three tools, and they are not interchangeable. JSON is a lossy subset of JavaScript values. structuredClone is the host algorithm (the same one postMessage uses). Recursion plus a WeakMap is the interview clone: arrays and plain objects, cycles that reuse the copy, maybe Date / Map / Set if you have time.

Key takeaways

  • JSON round-trip drops functions and undefined keys, stringifies Date, empties Map/Set, and throws on cycles.
  • structuredClone keeps cycles, Date, Map, Set, and binary data. Functions and DOM nodes throw DataCloneError.
  • Interview recursion: copy primitives as-is, branch on Array.isArray, walk own enumerable keys for objects.
  • A WeakMap from original to clone handles cycles and shared subtrees (copy.a === copy.b when a === b).
  • Preserve array holes with index in array. A JSON clone would have written null.

JSON is a subset, structuredClone is the host, recursion is the interview

Start by naming what you are not building. JSON.stringify is a serializer. Using it as a copier inherits every serializer rule.

Value JSON.parse(JSON.stringify(x)) structuredClone(x) Interview recursion
plain object / array copy of JSON-able fields copy copy
Date ISO string Date new Date(x.getTime()) if you handle it
Map / Set {} contents copied extra credit: iterate and clone entries
undefined in object key omitted kept kept if you assign it
function omitted (object) / null (array) throws same reference, or skip; do not pretend you cloned the function
cycle TypeError clone that cycles WeakMap hit returns the in-progress clone
class instance enumerable data, methods gone data, prototype is not the class own keys; say whether you keep Object.getPrototypeOf
function deepClone(value, seen = new WeakMap()) {
  if (value === null || typeof value !== 'object') {
    return value;
  }

  if (typeof value === 'function') {
    return value;
  }

  if (seen.has(value)) {
    return seen.get(value);
  }

  if (value instanceof Date) {
    const copy = new Date(value.getTime());
    seen.set(value, copy);
    return copy;
  }

  if (value instanceof Map) {
    const copy = new Map();
    seen.set(value, copy);
    value.forEach((entryValue, entryKey) => {
      copy.set(deepClone(entryKey, seen), deepClone(entryValue, seen));
    });
    return copy;
  }

  if (value instanceof Set) {
    const copy = new Set();
    seen.set(value, copy);
    value.forEach((entry) => {
      copy.add(deepClone(entry, seen));
    });
    return copy;
  }

  if (Array.isArray(value)) {
    const copy = [];
    seen.set(value, copy);
    for (let i = 0; i < value.length; i++) {
      if (i in value) {
        copy[i] = deepClone(value[i], seen);
      }
    }
    return copy;
  }

  const copy = Object.create(Object.getPrototypeOf(value));
  seen.set(value, copy);
  for (const key of Object.keys(value)) {
    copy[key] = deepClone(value[key], seen);
  }
  return copy;
}
const node = { id: 1, when: new Date('2026-11-08T00:00:00.000Z') };
node.self = node;
const copy = deepClone(node);
copy !== node; // true
copy.self === copy; // true
copy.self !== node; // true
copy.when instanceof Date; // true
copy.when.getTime() === node.when.getTime(); // true
copy.when !== node.when; // true

Register the clone in seen before walking children. If you set it after the loop, a cycle recurses forever because the original is never found. That is the same “pin the node first” idea as building a graph.

WeakMap keys must be objects. Primitives never go in seen; they returned on the first line. That is why you do not use a Map of all values: 1 and another 1 are not identity, and you would pin every object forever. WeakMap lets the original graph get collected.

Cycles vs diamonds

Stringify tracks the current path and throws on a back-edge, so a diamond { a: x, b: x } serializes x twice. A clone should share: copy.a === copy.b, and both distinct from x. The WeakMap is “have I already allocated the clone for this original?”, not a stack you pop. Do not seen.delete after the node. Deleting would split a diamond into two copies and still fail a cycle that points at an ancestor if you pop too early.

Arrays vs plain objects

typeof [] === 'object', so branch on Array.isArray. Walk indexes from 0 up to length and copy only when i in value. Holes stay holes and length is preserved. for...of skips holes and push densifies. Same trap as deep equality and map.

Object.keys on a plain object is own enumerable string keys. Symbol keys are extra credit (Object.getOwnPropertySymbols). Non-enumerable fields and getters that throw are extra credit. Inherited enumerable keys are usually not copied; you walk own keys and, if you care, set the prototype with Object.create.

What structuredClone does that you should not re-spec

Call structuredClone in production for supported types. It clones Date, RegExp, Map, Set, ArrayBuffer, Blob, and cycles. It throws on functions, DOM nodes, and most platform objects with identity. It does not clone the class prototype: a User instance comes back as a plain data object. Transfer semantics (transfer option) are out of scope for a whiteboard.

Do not implement clone in terms of JSON “because it is shorter” and then claim Date survived. The deep vs shallow copy page is the conceptual pair. The platform structured clone page is the host pair. This page is the function they ask you to type.

Footguns

  1. Cloning this of a bound function, or “deep cloning” a function at all. There is no useful deep copy of bytecode. structuredClone throws. Returning the same function is honest.
  2. RegExp lastIndex: copy source and flags, then set lastIndex if they care.
  3. Prototype: Object.create(null) vs {}. If the original had a null prototype, {} adds Object.prototype.
  4. Accessors: obj.foo may run a getter. Object.getOwnPropertyDescriptors plus defineProperties is the property-faithful clone; obj[key] is the value snapshot. Say which.
  5. const copy = value is not a clone. { ...value } is shallow.

Interview answer (out loud)

JSON.parse of JSON.stringify is a lossy subset: dates become strings, functions and undefined keys disappear, maps become empty objects, cycles throw. structuredClone is the host: it keeps cycles, Date, Map, and binary data, and it throws on functions. For the interview I recurse. Primitives return as-is. I put the original in a WeakMap pointing at the new array or object before I walk children, so a cycle returns the in-progress clone and a shared subtree stays shared. Arrays use index-in-object so holes survive. I would not pop the WeakMap the way stringify pops its path stack.

Further reading

Related guides