What is the difference between shallow copy and deep copy in JavaScript?
Advertisement
🧩 Scenario
Architecture Walkthrough
What Shallow Copy Does
A shallow copy creates a new object at the top level and copies each property value from the original. For primitive values (strings, numbers, booleans), the value itself is copied. For reference values (objects, arrays, functions), only the reference (memory address) is copied, not the object it points to. Both the original and the copy end up pointing to the same nested object.
This means mutating a nested object through the copy also mutates it through the original because they share the same reference. The copy is only independent at the top level. Every property that holds a primitive is independent; every property that holds an object is shared.
Shallow Copy Methods
Spread syntax ({ ...obj }), Object.assign({}, obj), and Array.prototype.slice() (for arrays) all produce shallow copies. They are equivalent in behavior for plain objects and arrays. The choice between them is stylistic: spread is the modern idiomatic form, Object.assign is common in older codebases, and slice() is useful for copying arrays into a new array reference.
Deep Copy: structuredClone and Its Predecessors
A deep copy recursively copies every nested object and array so that the copy shares no references with the original. structuredClone() was added to browsers in 2022 and Node.js 17 and is now the standard way to deep clone an object. It handles nested objects, arrays, Date, Map, Set, ArrayBuffer, and circular references correctly.
The older alternative, JSON.parse(JSON.stringify(obj)), fails in four important cases: Date objects are converted to strings, functions and undefined values are silently dropped, BigInt throws a serialization error, and circular references throw. It also cannot copy Map, Set, or Symbol keys. Use JSON.parse/stringify only for simple data objects with no special types.
Key Code Explained
const original = {
name: 'Ghazi',
address: { city: 'Mumbai', country: 'India' },
tags: ['react', 'typescript'],
};
// Shallow copy: top-level properties are independent, nested objects are shared
const shallow = { ...original };
shallow.name = 'Ali'; // independent — does not affect original.name
shallow.address.city = 'Delhi'; // SHARED — also changes original.address.city
shallow.tags.push('node'); // SHARED — also changes original.tags
console.log(original.address.city); // 'Delhi' — mutated through shallow copy
console.log(original.tags); // ['react', 'typescript', 'node']
// Deep copy with structuredClone: nothing is shared
const deep = structuredClone(original);
deep.address.city = 'Chennai';
deep.tags.push('graphql');
console.log(original.address.city); // 'Mumbai' — not affected
console.log(original.tags); // ['react', 'typescript', 'node'] — not affected
// JSON method: works for simple objects, fails for special types
const obj = { score: 42, createdAt: new Date(), action: undefined };
const jsonClone = JSON.parse(JSON.stringify(obj));
console.log(typeof jsonClone.createdAt); // 'string' — Date became a string
console.log('action' in jsonClone); // false — undefined was dropped
// structuredClone handles Date correctly
const cloned = structuredClone(obj);
console.log(cloned.createdAt instanceof Date); // true
// Note: structuredClone also drops functions and undefined values
// React state update pitfall: shallow copy must cover every changed level
const state = { user: { profile: { name: 'Ghazi', age: 28 } } };
// Wrong: only top-level is a new object, nested objects are still shared
const badUpdate = { ...state, user: { ...state.user } };
// state.user.profile and badUpdate.user.profile are the SAME object
// Correct: copy every level that changes
const goodUpdate = {
...state,
user: {
...state.user,
profile: { ...state.user.profile, name: 'Ali' },
},
};
The React state update example is the most practically important. A selector or memo comparison that checks state.user.profile === previousState.user.profile will return true for badUpdate even though you intended to change the profile, causing the component to skip re-rendering.
Tradeoffs
| Method | Deep copy | Handles Date/Map/Set | Handles circular refs | Performance |
|---|---|---|---|---|
| Spread / Object.assign | No | N/A (shallow only) | N/A | Fastest |
| JSON.parse/JSON.stringify | Yes | No (Date to string) | No (throws) | Slow for large |
| structuredClone | Yes | Yes | Yes | Moderate |
| Recursive custom utility | Yes | Configurable | Needs cycle detection | Depends on impl |
What Interviewers Actually Check
- Whether you can explain what shallow copy copies vs what it shares
- Whether you can name the methods that produce a shallow copy
- Whether you know the failure cases of
JSON.parse/JSON.stringify - Whether you know
structuredCloneas the modern standard for deep cloning - Whether you can explain why shallow copies in React state require copying every changed level
Follow-Up Questions
- How does Immer.js solve the problem of immutable nested updates without requiring manual spread at every level?
- When would you use a recursive
deepClonefunction instead ofstructuredClone? - What happens when you use spread to copy an array of objects and then modify one of the objects?
- How does
Object.freezeinteract with shallow vs deep copying? - Can you implement a shallow clone function that also handles arrays and nested null values correctly?
Common Candidate Mistakes
- Spreading a nested object and mutating the copy's nested property, then being surprised that the original also changed
- Using
JSON.parse/JSON.stringifyon objects containingDatevalues and not knowing theDatebecomes a string - Thinking
Object.assignis different from spread when both produce the same shallow copy - Not knowing that
Array.slice()only creates a new array reference at the top level, not a deep copy of the contained objects - Not knowing
structuredCloneexists and reaching forlodash.cloneDeepor a manual recursive function when the built-in handles most cases
Interview Readiness Checklist
Before you leave this question, make sure you can answer:
- Can you explain what shallow copy does and why nested objects remain shared references?
- Can you name three methods that produce a shallow copy?
- Can you explain the four failure cases of
JSON.parse/JSON.stringifydeep cloning? - Can you write a deep copy using
structuredCloneand explain what it cannot handle? - Can you explain why shallow copies in React state require copying at every changed level of nesting?
Summary
A shallow copy creates a new object with the same top-level property values. Primitives are independent in the copy; reference-type values are shared. Both the original and the copy point to the same nested objects, so mutating a nested property through either one affects both. Spread, Object.assign, and Array.slice are all shallow copy mechanisms with equivalent behavior.
A deep copy recursively duplicates every level so that the copy and original share no references. structuredClone() is the modern standard: it handles Date, Map, Set, ArrayBuffer, and circular references correctly. The older JSON.parse/JSON.stringify approach silently drops undefined and functions, converts Date to strings, and throws on circular references.
The most consequential real-world impact is in React and Redux state updates. Reducers and state setters must return a new object reference at every level that changed. A shallow copy at the top that leaves a nested object reference unchanged causes memoized selectors and React.memo to treat the state as unchanged, silently skipping re-renders.
Does JSON.parse(JSON.stringify(obj)) always work?
No. It fails for Dates (converts to string), functions (dropped), undefined (dropped), and circular references (throws).
Advertisement
Stay Updated
Get the latest frontend challenges, interview questions and tutorials delivered to your inbox.
Advertisement