Command Palette

Search for a command to run...

Hectal
PHASE 5Beginner ~19 min· topic 10 of 10

Topic 5.10

Copying: Shallow vs Deep

In one line

Assigning an array to another variable copies only the reference, so both names point at the same array; spread, slice and Array.from make a shallow copy (a new outer array sharing the same inner objects), and structuredClone makes a deep copy where nothing is shared.

Think of it like this

A shared Google Doc versus a photocopy. Giving a friend the link to your doc means you're both looking at the same document: if they type, you see it. That's assigning an array to another variable. A photocopy of a page is yours alone, but if the page has a sticky note saying "see the folder in the drawer", both copies still point at the same folder. That's a shallow copy. Copying the page and the folder and everything in it is a deep copy.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

Reference
The address of an array or object in memory. Variables hold this, not the array itself.
Alias
A second name for the same array: const b = a makes b an alias of a.
Shallow copy
A new outer array whose items are the same values; inner objects are still shared.
Deep copy
A copy of the array and everything inside it, all the way down; nothing is shared.
structuredClone
A built-in function that makes a deep copy of most data types.
Primitive
A simple value like a number, string or boolean, which is copied, not shared.
Immutable update
Making a new version of data with a change, instead of changing the original.
Circular reference
Data that contains itself somewhere inside, like an array that holds itself.

Step by step

01Assigning copies the reference

Two variables, one array.

app.jswhole filejavascript
const a = [1, 2];
const b = a;
b.push(3);
console.log(a); // [ 1, 2, 3 ]

02Shallow copy: new box, same contents

The outer array is new; objects inside are shared.

Shallow copy: new box, same contentsdiagram
Rendering diagram…

03Deep copy: nothing shared

structuredClone copies the array and every object inside it.

Deep copy: nothing shareddiagram
Rendering diagram…

04Pick by what's inside

Only primitives inside? [...arr] is a complete copy. Objects inside and you'll change them? Either structuredClone the whole thing, or better, update only the item you change with map and object spread.

app.jswhole filejavascript
const updated = team.map((p) =>
  p.name === "Ravi" ? { ...p, score: p.score + 1 } : p
);

Try it yourself

  1. 1

    Predict before you run

    In the first example, add const g = b; g.length = 0; at the end and log a, c and d. Which ones become empty?

  2. 2

    Copy a 2D array

    Make const grid = [[1, 2], [3, 4]];, copy it with [...grid], and set copy[0][0] = 99. Predict grid[0][0]. Then fix it with grid.map((row) => [...row]).

  3. 3

    Update without mutating

    In the second example, write an updated that renames Asha to "Asha K" without changing team. Check with team[0].name.

Code & diagrams

Same array or a copy? New tab

`b` is the same array as `a`. `c`, `d`, `e` and `f` are new arrays. Two arrays with equal contents are still not `===`.

Console

(nothing was printed)

Expected console output

[ 1, 2, 3, 4 ] true
[ 1, 2, 3, 4 ] [ 1, 2, 3, 4, 5 ] false
false
[ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ] [ 1, 2, 3, 4 ]
The shallow copy trap, and two fixes ES2018+ New tab

The shallow copy shared Asha's object, so her score changed in `team` too (the page shows 99). `structuredClone` shares nothing. The `map` + object spread (ES2018) update copies only Ravi's object and reuses Asha's.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

99
false true
7 0 false
7 8 true
structuredClone vs the JSON trick New tab

JSON turned the date into a string, the Map into `{}`, and `undefined` and `NaN` into `null`. `structuredClone` kept them all and even copied the array that contains itself. Functions can't be cloned.

Console

(nothing was printed)

Expected console output

[ '2024-01-15T10:30:00.000Z', {}, [ 1, [ 2 ] ], null, null ]
true 1 NaN
true false
DataCloneError

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

"Copying" with =

To keep a backup before sorting, write const backup = scores; scores.sort((a, b) => a - b);.

terminal
$ Console
── what you'll see ──
backup is sorted too, so the original order is lost.

Break #2

Shallow copy, deep change

Copy const draft = [...cart]; and then change draft[0].qty = 5; to preview a change.

terminal
$ the page shows
── what you'll see ──
The real cart also shows quantity 5, even though you only changed the draft.

Myth vs fact

Myth

const copy = original; copies the array.

Fact

It copies the reference. Both names point to one array, so a change through either is seen by both.

Myth

Spread makes a deep copy.

Fact

Spread, slice, Array.from and concat are all shallow: nested objects and arrays are shared.

Myth

JSON.parse(JSON.stringify(x)) is the right way to deep-copy.

Fact

It drops or changes dates, Maps, Sets, undefined, NaN and functions, and fails on circular data. structuredClone is the built-in, correct tool.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    structuredClone uses the structured clone algorithm, the same one used by postMessage to send data to Web Workers and by IndexedDB to store it. It's in all major browsers since 2022 and in Node.js 17+. It's defined in the HTML standard, not ECMAScript.

  • ▸

    Deep copying is O(size of the data) in time and memory. Copying large state on every change gets slow; immutable updates that copy only the changed path (structural sharing) keep it cheap. Libraries like Immer automate this.

  • ▸

    structuredClone drops the prototype of class instances (they come back as plain objects) and drops property getters, setters and non-enumerable properties. Errors, Dates, RegExps, Maps, Sets, ArrayBuffers and typed arrays are supported.

  • ▸

    Object.freeze(arr) prevents changes to the array itself, but is also shallow: objects inside can still change. A deep freeze needs a recursive function.

Remember this

  1. 1

    Arrays (like all objects) are stored by reference. A variable doesn't hold the array itself; it holds a reference, like an address, to where the array lives in memory. const b = a; copies the address, not the array. Now a and b are two names for one array: b.push(4) changes what a shows too, and a === b is true.

  2. 2

    Comparing arrays with === or == checks whether they are the same array, not whether they contain the same items: [1, 2] === [1, 2] is false. To compare contents, check length and each item (or for simple data, compare JSON.stringify results, with care).

  3. 3

    A shallow copy creates a new outer array with the same items. Ways to make one: [...arr] (spread), arr.slice(), Array.from(arr), arr.concat(). Changing the copy's slots (push, copy[0] = x) doesn't affect the original. But if an item is an object or array, the copy holds the same reference, so copy[0].score = 99 changes original[0].score too. For arrays of primitives (numbers, strings, booleans), a shallow copy is all you need.

  4. 4

    A deep copy copies everything, all the way down, so nothing is shared. structuredClone(value) (a browser and Node.js built-in since 2022, not part of ECMAScript itself) does this properly: it handles nested arrays and objects, Date, Map, Set, typed arrays and even circular references. It cannot copy functions or DOM elements (it throws a DataCloneError), and class instances come back as plain objects.

  5. 5

    The old trick JSON.parse(JSON.stringify(arr)) also deep-copies plain data, but loses things: Date becomes a string, Map and Set become {}, undefined and functions in arrays become null, NaN and Infinity become null, and circular references throw. Prefer structuredClone.

  6. 6

    Often you don't need a deep copy at all: copy only the path you change. To update one item, map the array and spread only the changed object: team.map((p) => (p.id === id ? { ...p, score: p.score + 1 } : p)). Unchanged items keep their references, which is cheap and is exactly what React and Redux expect (an immutable update). Phase 6 covers object spread and references in more depth.

Explain it without notes

01

What is the difference between a shallow copy and a deep copy of an array?

02

Why does [1, 2] === [1, 2] return false?

03

What can structuredClone copy that JSON.parse(JSON.stringify()) can't, and what can't it copy?

04

How do you update one object inside an array without mutating the original array?

Practice

01

Make a copy of const prices = [10, 20, 30]; that you can sort descending without changing prices, in two different ways.

02

Given const users = [{ id: 1, active: false }, { id: 2, active: false }];, return a new array where user 2 is active, without changing users, and prove it.

Trade-offs

  • ↔

    A deep copy is the simplest way to be sure nothing is shared, but costs time and memory proportional to the data; copying only the changed path is cheaper but needs more care.

  • ↔

    Mutating in place is fastest and fine for arrays a function created itself; for shared data (state, arguments), copies prevent bugs that are very hard to trace.

Done when you can

  • I can explain why const b = a doesn't copy an array.

  • I can make a shallow copy four ways and know what it shares.

  • I can make a deep copy with structuredClone and know its limits.

  • I know why the JSON trick loses data.

  • I can update one item in an array of objects without mutating anything.