Problem and Applicable Context
Implement deepClone(value). The input is a finite object graph from one JavaScript realm. It may contain primitives, arrays, plain objects whose prototype is Object.prototype or null, Date, RegExp, Map, and Set. Every supported source object must have a distinct object in the copy. If two source edges point to the same object, the corresponding copy edges must point to the same copied object. A cycle must not cause unbounded recursion.
The function must also copy every own data property, including non-enumerable and symbol-keyed properties, while preserving its descriptor. Preserve array holes, the time value of a Date, the source, flags, and lastIndex of a RegExp, both keys and values of a Map, and values of a Set.
Functions, accessor properties, WeakMap, WeakSet, proxies, typed arrays, array buffers, custom class instances, and unlisted built-ins are outside the contract and must cause a TypeError. The implementation is recursive, so it also assumes nesting will not exhaust the call stack. For production data that fits the platform's structured-cloneable types, evaluate structuredClone before turning this interview implementation into a general-purpose library.
What the Interviewer Is Evaluating
The first signal is whether the candidate defines the meaning of “deep clone.” JavaScript has no universal userland rule that can reproduce function closures, DOM nodes, private fields, proxies, and every built-in internal slot. A strong answer lists supported types, property semantics, cycle behavior, and failure behavior before writing recursion.
The second signal is recognizing an object graph rather than an object tree. Recursively allocating objects can copy a tree, but it cannot handle source.self = source, and it incorrectly turns source.a === source.b into two separate copies. The required state is not merely “visited”; it is “which copy belongs to this source object.”
The third signal is allocation order. Allocate an empty target and record the source-to-copy mapping before traversing outgoing edges. If children are copied before registration, the first back edge still finds no target and recursion continues around the cycle.
The fourth signal is an honest account of property and built-in boundaries. Object.entries misses symbol keys and non-enumerable properties. Reading source[key] can execute a getter. Giving a Date, Map, or Set an object with the same prototype does not reproduce its internal slots.
Questions to Clarify Before Answering
- Which types must be supported? Arrays and plain objects are enough for JSON-shaped data. Adding
Date,RegExp,Map, andSetrequires type-specific construction and traversal. Adding typed arrays or array buffers introduces buffer-copying and ownership choices. - Should cycles and duplicate references fail, be broken, or preserve topology? This problem preserves them, so it needs a source-to-copy map. A
WeakSetcan detect a repeat but cannot return the correct copy. - Do properties mean enumerable string keys only, or also symbols, non-enumerables, and descriptors? This contract chooses the latter and rejects accessors, avoiding both getter execution and shared getter or setter functions.
- Must custom classes and prototype chains be copied? This problem accepts only plain objects and the listed built-ins.
Object.create(instancePrototype)cannot reproduce private fields or constructor-established state, so presenting the result as a complete instance would be misleading. - Is this an interview algorithm or a production API? The interview implementation demonstrates a contract and graph invariant. Production code should compare
structuredClonetype coverage, transfer semantics, and metadata loss before choosing a controlled custom serializer. - What is the maximum nesting depth? Recursion uses
O(d)auxiliary stack. A chain that may contain one hundred thousand objects requires an explicit work stack and changes the implementation and tests.
30-Second Answer Framework
“I will define the supported types and treat the input as a graph. Primitives return directly. For each object, I check a WeakMap; on its first visit, I allocate and register an empty copy before copying properties or entries. Cycles and duplicate references then resolve to one copy. Date and RegExp are rebuilt, while functions, accessors, and unsupported objects throw. Expected time and copy space are O(V + E), with an O(d) recursion stack.”
Step-by-Step Deep Dive
JSON.parse(JSON.stringify(value)) is valid only for a narrower JSON contract. It fails on cycles and changes or loses undefined, BigInt, symbols, Date, RegExp, Map, Set, and special numeric values. Plain recursion gives more control, but without a source-to-copy mapping it still only handles trees.
The central invariant is: before traversing any supported object's properties or entries, seen.get(sourceObject) already equals the unique copy allocated for it.
function deepClone(input) {
const seen = new WeakMap();
function clone(value) {
if (typeof value === 'function') {
throw new TypeError('Functions are not supported');
}
if (value === null || typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return seen.get(value);
}
let result;
if (value instanceof Date) {
result = new Date(value.getTime());
seen.set(value, result);
copyOwnDataProperties(value, result);
return result;
}
if (value instanceof RegExp) {
result = new RegExp(value.source, value.flags);
result.lastIndex = value.lastIndex;
seen.set(value, result);
copyOwnDataProperties(value, result, new Set(['lastIndex']));
return result;
}
if (value instanceof Map) {
result = new Map();
seen.set(value, result);
for (const [key, item] of value) {
result.set(clone(key), clone(item));
}
copyOwnDataProperties(value, result);
return result;
}
if (value instanceof Set) {
result = new Set();
seen.set(value, result);
for (const item of value) {
result.add(clone(item));
}
copyOwnDataProperties(value, result);
return result;
}
if (Array.isArray(value)) {
result = new Array(value.length);
seen.set(value, result);
copyOwnDataProperties(value, result, new Set(['length']));
Object.defineProperty(
result,
'length',
Object.getOwnPropertyDescriptor(value, 'length'),
);
return result;
}
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) {
throw new TypeError('Unsupported object type');
}
result = Object.create(prototype);
seen.set(value, result);
copyOwnDataProperties(value, result);
return result;
}
function copyOwnDataProperties(source, target, skipped = new Set()) {
for (const key of Reflect.ownKeys(source)) {
if (skipped.has(key)) {
continue;
}
const descriptor = Object.getOwnPropertyDescriptor(source, key);
if (!('value' in descriptor)) {
throw new TypeError('Accessor properties are not supported');
}
descriptor.value = clone(descriptor.value);
Object.defineProperty(target, key, descriptor);
}
}
return clone(input);
}seen must store the source-to-copy relationship, not just a visited bit. Suppose source.first and source.second both point to shared. The first visit allocates and registers sharedCopy; the second returns that same object, preserving aliasing. If source.self points back to source, the root was registered before its properties were copied, so the back edge points to the root copy.
WeakMap fits because every key is an object and the algorithm never needs enumeration. A regular Map would also be correct inside one invocation and would not automatically leak forever after the function returns. Weak keys fit the association's lifetime, but “prevents memory leaks” is not a proof that cycles are handled correctly.
Reflect.ownKeys returns string and symbol keys, including non-enumerable properties. The code reads descriptors rather than values, so it does not proactively invoke a getter; accessors fail according to the contract. It recursively replaces a data descriptor's value and defines the property with its writable, enumerable, and configurable flags. Array length is a special non-configurable property, so other keys are copied first and its descriptor is restored last. Sparse holes are not accidentally converted into elements whose value is undefined.
Date, RegExp, Map, and Set contain internal state that ordinary property copying cannot reach. The implementation rebuilds the time value, regular expression source, flags, and lastIndex, map keys and values, and set values. Each container is registered before iteration, so a map or set can participate in a cycle. The code assumes same-realm objects; cross-realm instanceof checks are unreliable and call for platform cloning or stricter brand checks.
The boundary remains explicit. This code does not preserve frozen, sealed, or non-extensible state, does not clone the prototype chain, and does not support accessors, private fields, proxies, buffers, or unlisted built-ins. structuredClone supports more platform types, cycles, and duplicate identity, but it also excludes functions and does not preserve property descriptors, getters, setters, the prototype chain, or RegExp.lastIndex. These contracts are not interchangeable merely because both are called deep clones.
Let V be the number of distinct objects and E the number of references contributed by own properties, map entries, and set elements. Under the usual average-performance assumption for built-in mappings, traversal work and copy space are O(V + E) because each source object is expanded once. The recursive call stack is O(d), where d is the longest nested path. ECMAScript requires only average sublinear access for Map, Set, and WeakMap; it does not promise strict O(1) operations in every implementation.
Tests must exercise graph structure rather than compare serialized text: a self-cycle; two properties sharing one child; a map key also referenced by another property; a set containing a shared object; a sparse array; a null-prototype object; non-enumerable and symbol-keyed data properties; Date; RegExp with a nonzero lastIndex; failures for functions, accessors, and custom classes; and isolation after mutating the copy. A very deep acyclic chain should also test the recursion boundary.
High-Quality Sample Answer
“I will scope this to a finite, same-realm object graph containing primitives, Array, plain Object, Date, RegExp, Map, and Set. Functions, accessors, weak collections, buffers, and custom classes throw because the problem does not define testable copy semantics for them.
The key is preserving object identity relationships, not recursion by itself. I keep a WeakMap<source, copy>. Whenever I see an object, I check the map first. On its first visit, I allocate an empty copy and register it before copying properties or entries. A self-cycle then resolves to the current copy, and two edges to one source object resolve to one target object. Map keys and values and Set elements use the same clone path, so aliasing across containers is preserved.
For ordinary properties I use Reflect.ownKeys and descriptors. That preserves symbols, non-enumerables, and data descriptor flags without quietly executing getters. Date, RegExp, Map, and Set get type-specific reconstruction. Counting distinct objects and reference edges, expected work and copy space are O(V + E), and the call stack is O(d). In production I would use structuredClone when its support matrix fits, while documenting that it does not preserve descriptors, prototypes, or RegExp lastIndex.”
Common Mistakes
- Serialize and parse JSON → cycles throw, several valid JavaScript values are lost or changed, and shared references split → use a JSON-only contract, a graph algorithm, or
structuredCloneas appropriate. - Recursively copy only arrays and objects → self-cycles recurse forever and repeated references become separate objects → map every source object to one copy.
- Copy children before inserting into
seen→ the first back edge still has no mapping → allocate and register the empty copy before expanding edges. - Track visits with a
WeakSet→ it detects a repeat but cannot tell the algorithm which copy to return → useWeakMap<source, copy>. - Use
for...inorObject.entriesfor every property → the former includes inherited enumerable properties, while the latter misses symbols and non-enumerables → use own descriptors andReflect.ownKeyswhen the contract requires them. - Read
source[key]directly → a getter may perform side effects or throw, changing the observable behavior of cloning → inspect descriptors and define an explicit accessor policy. - Create every target with
Object.create(proto)→ internal slots for Date, Map, and Set remain absent, and custom class private fields are missing → rebuild supported built-ins and reject other types. - Claim strict
O(V + E)→ ECMAScript does not guarantee constant-time Map, Set, or WeakMap operations, and recursion may overflow → state the average-performance assumption andO(d)stack boundary.
Follow-Up Questions and Responses
Follow-up 1: Why preserve duplicate references instead of only preventing cycles?
Identity can carry application meaning. If order.customer === cache.currentCustomer, cloning them independently makes that equality false in the copy, and mutations through one copied path are no longer observable through the other. A one-to-one source-to-copy mapping preserves both cycles and aliasing. The test should assert copy.first === copy.second; merely asserting that cloning did not overflow proves too little.
Follow-up 2: How would you handle a chain one hundred thousand objects deep?
Keep the same seen invariant but replace recursive calls with an explicit work stack. Allocate and register a copy on first encounter, then push frames containing the source container, target container, and pending keys or entries. An iterative loop processes those frames. Time and heap usage still grow with V + E, but the auxiliary state moves from the language call stack to a controlled heap structure. Date and RegExp finish immediately; Object, Array, Map, and Set require follow-up frames.
Follow-up 3: What changes if ArrayBuffer can be copied or transferred?
The API needs an explicit option. Copying allocates an equal-length buffer and copies bytes. Transferring invalidates the source buffer, so it is an ownership move with side effects and cannot be hidden inside deepClone. The platform already defines this through structuredClone(value, { transfer: [...] }). A custom implementation that cannot detach storage should reject transfer rather than return two views over one buffer and label the result a deep copy.
Follow-up 4: How would you support custom classes, getters, and private fields?
General reflection cannot read private fields or reproduce closures. Retaining getters and setters shares functions and closures; evaluating a getter can cause side effects. A defensible extension is a serializer registry: each class supplies serialize and deserialize functions that rebuild its invariants, and its adapter decides whether accessors are retained, evaluated, or rejected. Without an adapter, throwing is safer than creating an object for which instanceof is true while its internal state is broken.