Deep vs Shallow Copy
Shallow copy shares nested refs; deep copy clones the graph — spread, structuredClone, JSON limits, and React state implications.
- javascript
- deep-copy
- shallow-copy
- immutability
Assigning an object copies the reference, not the value. A shallow copy duplicates the top level only — nested objects still shared. A deep copy clones the whole reachable graph (within limits). Most production bugs are shallow-when-you-needed-deep, or deep-when-shallow-was-enough and expensive.
Same reference
const a = { user: { name: 'Ada' } };
const b = a;
b.user.name = 'Grace';
a.user.name; // 'Grace' — same object
Shallow copy
const original = {
n: 1,
nested: { ok: true },
tags: ['js'],
};
const spread = { ...original };
const assign = Object.assign({}, original);
const fromEntries = Object.fromEntries(Object.entries(original));
const arrCopy = original.tags.slice(); // or [...tags]
spread.n = 2;
original.n; // 1 — top-level primitive copied
spread.nested.ok = false;
original.nested.ok; // false — nested shared
Arrays: [...arr], arr.slice(), Array.from(arr) are shallow.
Deep copy options
structuredClone (prefer)
const deep = structuredClone(original);
deep.nested.ok = true;
original.nested.ok; // false — independent
// supports: Date, Map, Set, ArrayBuffer, cyclic refs (in modern engines)
const withCycle = { a: 1 };
withCycle.self = withCycle;
structuredClone(withCycle); // works
Does not clone functions, DOM nodes, or symbols as keys the way you might hope — those throw or strip depending on type.
JSON round-trip (lossy)
const deep = JSON.parse(JSON.stringify(original));
| Lost or changed | Example |
|---|---|
undefined, functions, symbols |
dropped |
Date |
becomes string |
NaN, Infinity |
null |
Map / Set |
plain objects / empty |
| Cycles | throw |
Fine for simple JSON-shaped data. Wrong for real app state graphs.
Lodash cloneDeep / custom
Use when you need custom handling. Rolling your own recursive clone is a common interview task — remember cycles with a WeakMap.
function deepClone(value, seen = new WeakMap()) {
if (value === null || typeof value !== 'object') return value;
if (seen.has(value)) return seen.get(value);
if (value instanceof Date) return new Date(value);
if (value instanceof Map) {
const m = new Map();
seen.set(value, m);
value.forEach((v, k) => m.set(deepClone(k, seen), deepClone(v, seen)));
return m;
}
if (Array.isArray(value)) {
const arr = [];
seen.set(value, arr);
value.forEach((v, i) => {
arr[i] = deepClone(v, seen);
});
return arr;
}
const out = Object.create(Object.getPrototypeOf(value));
seen.set(value, out);
for (const key of Reflect.ownKeys(value)) {
out[key] = deepClone(value[key], seen);
}
return out;
}
React / state angle
// wrong: mutate then setState same ref
state.items.push(item);
setItems(state.items); // may not re-render
// shallow copy top array + new item
setItems([...items, item]);
// nested update — copy each level you change
setUser({
...user,
profile: { ...user.profile, bio: next },
});
You rarely need a full deep clone of the entire store — copy the path you write (immutability patterns).
Interview answer
“Assignment shares references. Shallow copies like spread clone one level; nested objects remain shared. structuredClone is the modern deep copy for many built-in types. JSON.parse/stringify is lossy. In UI state I copy only the update path, not the whole tree.”
Related
Property descriptors and prototypes
const base = Object.create({ inherited: 1 });
base.own = 2;
const shallow = { ...base }; // only own enumerable: { own: 2 }
const full = structuredClone({ own: 2 }); // plain object, no prototype copy
Spread and Object.assign copy enumerable own properties only — getters are invoked and become data properties. structuredClone also does not clone prototype methods; you get data. If you need a clone that preserves class instances, write a type-aware clone or use a library designed for your domain objects — generic deep clone will surprise you.
Further reading
Related guides
- Immutability Patterns in JSUpdate state without mutating — spread paths, arrays, structural sharing ideas, freeze, and libraries when nested updates hurt.
- AbortControllerCancel fetch and other async work with AbortController and AbortSignal — timeouts, race conditions, and cleanup when components unmount.
- Array find, some, every, includesShort-circuiting array predicates: find, findIndex, some, every, and includes — when to use each and common interview traps.
- Array map, filter, reducemap, filter, and reduce as the core transform toolkit — immutability, chaining costs, reduce patterns, and when a plain loop is clearer.
- Arrow Functions Deep DiveLexical this, no arguments object, no construct, concise bodies — when arrows help and when methods and generators need classic functions.