Shallow copy vs deep copy in JavaScript
Shallow copy creates a new object with copies of top-level properties, but nested objects stay shared. Deep copy clones the full structure at every level so nothing is shared between the original and the clone.
Theory
TL;DR
- Shallow copy is like photocopying a letter with attached photos: the letter is new, but both copies point to the same photos
- Deep copy photocopies the photos too - nothing shared
{ ...obj }andObject.assign()are shallow;structuredClone()andlodash.cloneDeep()are deep- Flat object with no nesting? Shallow is enough. Nested state or API payloads? Use deep.
structuredClone()is the modern default for deep copying, available in all browsers since 2022 and Node.js 17+
Quick example
const original = { name: 'Alice', scores: [90, 85] };
const shallow = { ...original }; // shallow copy
const deep = structuredClone(original); // deep copy
shallow.name = 'Bob';
shallow.scores[0] = 100;
console.log(original.name); // 'Alice' - string copied by value
console.log(original.scores[0]); // 100 - array is shared!
console.log(deep.scores[0]); // 90 - fully independentshallow.name and original.name are independent because strings are primitives copied by value. But shallow.scores and original.scores point to the same array in memory, so any mutation on one affects the other.
Key difference
Shallow copy allocates a new object and iterates enumerable own properties. Primitives (strings, numbers, booleans) are copied by value. Objects and arrays at any deeper level are assigned by reference - both the original and the copy point to the same memory location. Deep copy traverses the full structure recursively, creating new objects and arrays at every level so the two structures share nothing.
When to use
- Config object with no nested data - shallow with
{ ...obj }orObject.assign - React component state with nested fields like
address.cityorcoords[]- deep copy to isolate updates and avoid stale references - API response you cache and then modify -
structuredClonekeeps the cache intact - Performance-critical loops where nested data is read-only - shallow wins here (top-level iteration vs full recursive traversal)
Comparison table
| Method | Type | Functions | Date | Circular refs | Best for |
|---|---|---|---|---|---|
{ ...obj } | Shallow | kept | kept | N/A | Flat objects, React state spreads |
Object.assign() | Shallow | kept | kept | N/A | Merging flat configs |
structuredClone() | Deep | throws | preserved | handled | Modern apps, Node 17+, Worker messages |
JSON.parse/stringify | Deep | stripped | becomes string | throws | Simple objects with no special types |
lodash.cloneDeep() | Deep | cloned | preserved | handled | Legacy codebases, complex nested data |
How V8 handles this
V8 represents objects as hash maps with property descriptors and pointers. The spread operator iterates enumerable own properties via the [[GetOwnProperty]] internal method, copies primitive values directly, and reassigns pointers for nested objects without touching them. structuredClone() uses the HTML Structured Clone Algorithm: it traverses the full tree, creates new objects at every level, and tracks circular references with an internal map. That same tracking is why it throws on functions - they are not serializable by the algorithm. lodash.cloneDeep tracks visited nodes using a stack and creates new objects via Object.create(null), making it the most permissive option for edge cases.
Common mistakes
1. Assuming spread copies nested objects
const state = { user: { prefs: [1, 2] } };
const copy = { ...state };
copy.user.prefs.push(3);
console.log(state.user.prefs); // [1, 2, 3] - original mutatedSpread only goes one level deep. copy.user and state.user are the same reference. Fix: const copy = structuredClone(state).
2. Using JSON.parse/stringify with Dates or functions
const obj = { name: 'Alice', created: new Date(), greet: () => 'hi' };
const copy = JSON.parse(JSON.stringify(obj));
console.log(typeof copy.created); // 'string' - Date became a string
console.log(copy.greet); // undefined - function strippedstructuredClone preserves Date correctly. For functions, there is no built-in solution - use lodash.cloneDeep or handle them manually.
3. Using .slice() and thinking nesting is handled
const arr = [1, [2, 3]];
const copy = arr.slice(); // shallow - nested arrays still shared
copy[1][0] = 99;
console.log(arr[1][0]); // 99 - original mutated.slice(), spread, and Array.from all create shallow copies of arrays. None of them touch nested arrays.
4. Circular references blow up JSON
const obj = { name: 'test' };
obj.self = obj;
JSON.parse(JSON.stringify(obj)); // Throws: Converting circular structure to JSON
structuredClone(obj); // Works fineIf your data might be self-referential, structuredClone or lodash.cloneDeep are the only safe options.
5. Non-enumerable properties disappear on spread
const obj = { a: 1 };
Object.defineProperty(obj, 'b', { value: 2, enumerable: false });
const copy = { ...obj };
console.log(copy.b); // undefinedSpread and Object.assign skip non-enumerable properties. Easy to miss when copying objects that have hidden metadata attached via defineProperty.
Real-world usage
- React: spread for flat state updates (
{ ...user, name: 'Bob' });structuredClonefor nested form data before submission - Redux Toolkit:
createSlicedeep clones initial state internally to protect the store from accidental mutations - next-auth.js: uses
lodash.cloneDeepfor session objects passed through middleware layers - Express:
express-validatorclonesreq.bodybefore validation - shallow for flat bodies, deep for nested schemas - Node.js workers:
structuredCloneis the standard way to pass data toWorkerthreads without shared memory
Follow-up questions
Q: What prints here? const a = [1]; const b = [a]; const c = [...b]; c[0][0] = 99; console.log(a[0]);
A: 99. Spreading b is shallow, so c[0] and a are the same array. The mutation goes all the way back to a.
Q: What does structuredClone do with functions?
A: It throws a DataCloneError. Functions are not serializable by the Structured Clone Algorithm. Use lodash.cloneDeep or a custom recursive clone if the object contains functions.
Q: How does lodash.cloneDeep handle circular references?
A: It tracks visited objects using a stack. When it encounters an already-visited reference, it reuses the already-cloned version instead of recursing forever. JSON fails completely here; structuredClone handles it with an internal reference map.
Q: Is Object.assign({}, obj) different from { ...obj }?
A: For plain flat objects the result is the same. The difference: Object.assign invokes setters on the target if they exist; spread does not. Both call getters on the source.
Q: You need to pass a deeply nested 10,000-node object tree to a Web Worker. What do you use and why?
A: Pass it directly via postMessage - workers use the Structured Clone Algorithm automatically, the same one structuredClone uses. Avoid JSON round-trips because they lose type info for Dates, Maps, Sets, and ArrayBuffers. For truly large payloads, look at Transferable objects for zero-copy transfers.
Examples
Shallow copy mutation in React state
const [user, setUser] = useState({
name: 'Alice',
address: { city: 'NY', coords: [40.7, -74.0] }
});
// Spread creates a new top-level object, but address is still shared
const updated = { ...user, name: 'Bob' };
setUser(updated);
// Some other code mutates updated.address later
updated.address.city = 'LA';
console.log(user.address.city); // 'LA' - the original state object was affectedThis is one of the most common bugs in React apps. The spread { ...user } creates a new top-level object, but updated.address and user.address are still the same reference in memory. Any mutation to the nested object affects both.
Deep copy with structuredClone
const [user, setUser] = useState({
name: 'Alice',
address: { city: 'NY', coords: [40.7, -74.0] }
});
// Fully isolated copy - nothing shared
const updated = structuredClone(user);
updated.address.city = 'LA';
updated.address.coords[0] = 34.0;
setUser(updated);
console.log(user.address.city); // 'NY' - original untouched
console.log(user.address.coords[0]); // 40.7 - original untouchedstructuredClone traverses the full tree. Dates, Maps, Sets, and ArrayBuffers are all preserved with their correct types. The one thing to watch: if the object contains functions, it throws a DataCloneError.
Circular references and prototype chain (senior level)
const original = { name: 'test' };
original.self = original; // circular reference
// JSON fails immediately
try {
JSON.parse(JSON.stringify(original));
} catch (e) {
console.log(e.message); // Converting circular structure to JSON
}
// structuredClone handles it correctly
const cloned = structuredClone(original);
console.log(cloned !== original); // true - different object
console.log(cloned.self === cloned); // true - self-reference preserved inside clone
console.log(cloned.self !== original); // true - no link back to originalThe circular reference in cloned points back to cloned itself, not to the original. That is correct behavior for a true deep clone. I've seen this trip up senior candidates who assumed structuredClone would fail on cycles the same way JSON does.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.