JavaScriptMedium

Shallow copy vs deep copy

01

The Short Answer

A shallow copy duplicates only the top-level properties of an object — nested objects and arrays are still shared references pointing to the same memory. A deep copy recursively duplicates everything, creating a completely independent clone where no references are shared. Modifying a nested property in a shallow copy affects the original; modifying anything in a deep copy never does.

02

Why This Matters

In JavaScript, objects and arrays are reference types. When you assign an object to a new variable, you're not copying it — you're creating another pointer to the same data in memory. This means mutations through one variable affect the other. Copying is how you break that link, but the depth of the copy determines how independent the clone really is.

The code below demonstrates the fundamental problem. Both original and copy point to the same object in memory. Changing one changes the other because no copy was made at all — just a reference assignment.

reference-problem.tstypescript
const original = { name: 'Alice', address: { city: 'NYC' } };

// This is NOT a copy — it's another reference to the same object
const copy = original;

copy.name = 'Bob';
console.log(original.name); // 'Bob' — both point to the same object

// To actually copy, you need shallow or deep copy techniques
03

Shallow Copy

A shallow copy creates a new object and copies all top-level properties. Primitive values (strings, numbers, booleans) are duplicated by value. But nested objects and arrays are copied by reference — the new object's nested properties still point to the same objects in memory as the original.

📋

The Photocopied Address Book

A shallow copy is like photocopying an address book. You get a new book with the same entries, but the entries still point to the same physical houses. If someone renovates their house, both books now reflect the change because they reference the same house.

There are several ways to create a shallow copy. The spread operator and Object.assign are the most common. Notice how modifying a top-level primitive on the copy doesn't affect the original, but modifying a nested object does — because both copies share the same nested reference.

shallow-copy.tstypescript
const original = {
  name: 'Alice',
  age: 30,
  address: { city: 'NYC', zip: '10001' },
  hobbies: ['reading', 'hiking'],
};

// Shallow copy methods
const copy1 = { ...original };              // Spread operator
const copy2 = Object.assign({}, original);  // Object.assign
const copy3 = Array.from(original.hobbies); // For arrays
const copy4 = [...original.hobbies];        // Array spread

// Top-level primitives are independent
copy1.name = 'Bob';
copy1.age = 25;
console.log(original.name); // 'Alice' ✅ — not affected
console.log(original.age);  // 30 ✅ — not affected

// Nested objects are SHARED references
copy1.address.city = 'LA';
console.log(original.address.city); // 'LA' 💥 — original mutated!

copy1.hobbies.push('gaming');
console.log(original.hobbies); // ['reading', 'hiking', 'gaming'] 💥

This is the most common source of bugs with shallow copies. Developers spread an object thinking they have an independent clone, then accidentally mutate nested data that's shared with the original.

04

Deep Copy

A deep copy recursively clones every level of nesting, creating completely independent objects all the way down. No references are shared between the original and the clone. Modifying any property at any depth in the copy has zero effect on the original.

structuredClone (modern, recommended)

structuredClone() is the built-in deep copy function available in all modern browsers and Node.js 17+. It handles nested objects, arrays, Maps, Sets, Dates, RegExps, ArrayBuffers, and even circular references. It's the correct default choice for deep copying.

structured-clone.tstypescript
const original = {
  name: 'Alice',
  address: { city: 'NYC', zip: '10001' },
  hobbies: ['reading', 'hiking'],
  metadata: { createdAt: new Date(), tags: new Set(['admin']) },
};

const deepCopy = structuredClone(original);

// Everything is independent — no shared references
deepCopy.address.city = 'LA';
deepCopy.hobbies.push('gaming');

console.log(original.address.city); // 'NYC' ✅ — untouched
console.log(original.hobbies);      // ['reading', 'hiking'] ✅

// Handles special types correctly
console.log(deepCopy.metadata.createdAt instanceof Date); // true
console.log(deepCopy.metadata.tags instanceof Set);       // true

JSON.parse(JSON.stringify()) — the old way

Before structuredClone, the common hack was serializing to JSON and parsing back. This works for plain objects but silently breaks on Dates (become strings), undefined (dropped), functions (dropped), Maps/Sets (become empty objects), RegExp (becomes empty object), and circular references (throws). It's unreliable and should be avoided in favor of structuredClone.

json-hack.tstypescript
const original = {
  name: 'Alice',
  createdAt: new Date('2024-01-01'),
  callback: () => console.log('hi'),
  value: undefined,
  pattern: /test/gi,
};

const copy = JSON.parse(JSON.stringify(original));

console.log(copy.createdAt);  // '2024-01-01T00:00:00.000Z' — string, not Date!
console.log(copy.callback);   // undefined — function lost!
console.log(copy.value);      // undefined key is gone entirely
console.log(copy.pattern);    // {} — RegExp lost!

// structuredClone handles all of these correctly (except functions)
05

Comparison

AspectShallow CopyDeep Copy
Top-level primitivesIndependent ✅Independent ✅
Nested objects/arraysShared reference ⚠️Independent ✅
PerformanceFast — O(n) for top-level keysSlower — recursive traversal
Circular referencesN/A (just copies the ref)structuredClone handles it; JSON throws
FunctionsCopied as referenceNot supported by structuredClone or JSON
Use caseImmutable updates to flat objectsFull independence needed at all depths
06

Shallow Copy in React State Updates

React requires immutable state updates — you must return a new object reference for React to detect the change. For flat state, a shallow copy with spread is sufficient. For nested state, you need to spread at every level of nesting you're modifying. This is where the shallow vs deep distinction becomes a daily concern.

react-state-updates.tsxtypescript
const [user, setUser] = useState({
  name: 'Alice',
  address: { city: 'NYC', zip: '10001' },
  hobbies: ['reading'],
});

// ❌ Mutating nested state — React won't detect the change
user.address.city = 'LA';
setUser(user); // Same reference — no re-render!

// ❌ Shallow spread only — nested mutation still affects previous state
setUser({ ...user, address: { ...user.address, city: 'LA' } }); // ✅ Correct!

// ✅ Correct pattern — spread at every level you're changing
setUser(prev => ({
  ...prev,
  address: { ...prev.address, city: 'LA' },
  hobbies: [...prev.hobbies, 'gaming'],
}));

When deep nesting gets painful

If your state is deeply nested and updates are verbose, consider using Immer (via useImmer hook) which lets you write mutations that produce immutable updates under the hood. Or flatten your state structure so shallow spreads are sufficient.

07

Common Mistakes

🪞

Assuming spread creates a deep copy

The spread operator (`{...obj}`) only copies one level deep. Developers often spread an object and assume nested data is independent, then get confused when mutations propagate to the original.

Use `structuredClone()` when you need full independence, or spread at every nested level you're modifying.

🐌

Deep copying when shallow is sufficient

Deep copying is expensive for large objects. If you only need to change a top-level property, a shallow copy is faster and perfectly safe for that use case.

Match the copy depth to your needs. Shallow copy for flat updates, deep copy only when you need full independence at nested levels.

08

Why Interviewers Ask This

This question tests your understanding of JavaScript's reference model and how it impacts real-world code — especially React state management. Interviewers want to see that you know the difference between value and reference types, can identify when shared references cause bugs, know the available copy techniques and their limitations, and understand the performance tradeoffs. It's a fundamental concept that affects every non-trivial JavaScript application.

Quick Revision Cheat Sheet

Shallow copy: New top-level object, nested refs shared — spread, Object.assign

Deep copy: Fully independent at all depths — structuredClone()

JSON hack: Loses Dates, functions, undefined, Maps, Sets — avoid

React state: Spread at every nesting level you modify

Performance: Shallow is O(keys), deep is O(total nodes) — use the minimum needed