What is the difference between shallow copy and deep copy in JavaScript?

Advanced14 min interview
Skills tested:
What shallow copy copies vs what it shares by referenceSpread, Object.assign, and Array.slice as shallow copy mechanismsstructuredClone as the modern deep copy solutionJSON.parse/JSON.stringify limitations for deep cloningWhy shallow copies cause unexpected mutation bugs in React and Redux state

Advertisement

🧩 Scenario

In a real codebase, the shallow vs deep copy distinction matters most in state management. Redux and React state updates require producing a new object reference at each level that changed. A shallow copy at the top level that leaves nested objects pointing to the same references can cause components to not re-render because the reference comparison at the nested level does not see a change.

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

MethodDeep copyHandles Date/Map/SetHandles circular refsPerformance
Spread / Object.assignNoN/A (shallow only)N/AFastest
JSON.parse/JSON.stringifyYesNo (Date to string)No (throws)Slow for large
structuredCloneYesYesYesModerate
Recursive custom utilityYesConfigurableNeeds cycle detectionDepends 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 structuredClone as the modern standard for deep cloning
  • Whether you can explain why shallow copies in React state require copying every changed level

Follow-Up Questions

  1. How does Immer.js solve the problem of immutable nested updates without requiring manual spread at every level?
  2. When would you use a recursive deepClone function instead of structuredClone?
  3. What happens when you use spread to copy an array of objects and then modify one of the objects?
  4. How does Object.freeze interact with shallow vs deep copying?
  5. 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.stringify on objects containing Date values and not knowing the Date becomes a string
  • Thinking Object.assign is 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 structuredClone exists and reaching for lodash.cloneDeep or 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.stringify deep cloning?
  • Can you write a deep copy using structuredClone and 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.

Related Videos
Watch these videos to learn more about this topic
Frequently Asked Questions

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