On this page
Deep Clonehigh-yield
Last reviewed 22 Sept 2026
Problem
Implement deepClone(value) returning a copy that shares no mutable objects with the original:
const a = { n: 1, list: [1, { x: 2 }], when: new Date(0), tags: new Set(['js']) };a.self = a; // circular referenceconst b = deepClone(a);b.list[1].x = 99; // a.list[1].x is still 2b.self === b; // true — the cycle points at the copyClarifying questions
- Which types must be supported? Plain objects and arrays at minimum; ask about
Date,RegExp,Map,Set. - Circular references? Yes — they are the main follow-up.
- Keep the prototype of class instances? Usually yes.
- Functions? Copy by reference — cloning code is not meaningful.
- Why not
JSON.parse(JSON.stringify(x))? It dropsundefined, functions and symbols, turns dates into strings andNaNintonull, and throws on cycles.
Approach
Primitives and functions are returned as they are. For objects, create an empty container of the same kind, record it in a WeakMap before recursing, then copy the contents. When a value is seen again, return the recorded copy — this handles cycles and shared references.
Step-by-step build
Step 1 — objects and arrays
function deepClone(value) { if (value === null || typeof value !== 'object') return value; const copy = Array.isArray(value) ? [] : {}; for (const key of Object.keys(value)) copy[key] = deepClone(value[key]); return copy;}Step 2 — circular references
function deepClone(value, seen = new WeakMap()) { if (value === null || typeof value !== 'object') return value; if (seen.has(value)) return seen.get(value); const copy = Array.isArray(value) ? [] : {}; seen.set(value, copy); // register BEFORE recursing for (const key of Object.keys(value)) copy[key] = deepClone(value[key], seen); return copy;}Step 3 — special types, prototypes and symbol keys
if (value instanceof Date) return new Date(value.getTime());if (value instanceof RegExp) return new RegExp(value.source, value.flags);if (value instanceof Map) { /* new Map, clone each key and value */ }if (value instanceof Set) { /* new Set, clone each value */ }const copy = Array.isArray(value) ? [] : Object.create(Object.getPrototypeOf(value));for (const key of Reflect.ownKeys(value)) { /* includes symbol keys */ }Final code
function deepClone(value, seen = new WeakMap()) { if (value === null || typeof value !== 'object') return value; // primitives, functions if (seen.has(value)) return seen.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 copy = new Map(); seen.set(value, copy); value.forEach((v, k) => copy.set(deepClone(k, seen), deepClone(v, seen))); return copy; }
if (value instanceof Set) { const copy = new Set(); seen.set(value, copy); value.forEach((v) => copy.add(deepClone(v, seen))); return copy; }
const copy = Array.isArray(value) ? new Array(value.length) : Object.create(Object.getPrototypeOf(value)); seen.set(value, copy); for (const key of Reflect.ownKeys(value)) { if (Array.isArray(value) && key === 'length') continue; const desc = Object.getOwnPropertyDescriptor(value, key); if (desc && desc.enumerable) copy[key] = deepClone(value[key], seen); } return copy;}Edge cases
- Cycles (
a.self = a) and shared references ({ x: o, y: o }) are preserved: both keys point at one copy. DateandRegExpget real copies, not{}.- Class instances keep their prototype, so methods still work.
- Symbol keys are copied; non-enumerable properties are skipped (like spread).
- Very deep structures can overflow the call stack — an iterative version with an explicit stack avoids that.
Follow-ups
structuredClone: the built-in handles most of this — but not functions, DOM nodes or class prototypes. When would you still write your own?- Why
WeakMapand notMapforseen? The cache should not keep objects alive after the clone finishes. - Clone getters and setters as accessors instead of values — copy the descriptors with
Object.defineProperty. - Typed arrays,
ArrayBuffer,Error: extend the type checks.
Common mistakes
- Registering the copy in
seenafter recursing, which still loops forever on cycles. typeof value === 'object'without thenullcheck.for…in, which copies inherited properties.- Presenting
JSON.parse(JSON.stringify(x))as the answer without its limits.
Related
- Next: deep equal uses the same traversal.
- Shallow copies: Object.assign.