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 = amakesban alias ofa. - 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.
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.
03Deep copy: nothing shared
structuredClone copies the array and every object inside it.
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.
const updated = team.map((p) =>
p.name === "Ravi" ? { ...p, score: p.score + 1 } : p
);Try it yourself
- 1
Predict before you run
In the first example, add
const g = b; g.length = 0;at the end and loga,candd. Which ones become empty? - 2
Copy a 2D array
Make
const grid = [[1, 2], [3, 4]];, copy it with[...grid], and setcopy[0][0] = 99. Predictgrid[0][0]. Then fix it withgrid.map((row) => [...row]). - 3
Update without mutating
In the second example, write an
updatedthat renames Asha to"Asha K"without changingteam. Check withteam[0].name.
Code & diagrams
`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 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
Console
Nothing logged yet.Expected console output
99
false true
7 0 false
7 8 trueJSON 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
DataCloneErrorBreak 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);.
Break #2
Shallow copy, deep change
Copy const draft = [...cart]; and then change draft[0].qty = 5; to preview a change.
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.
- ▸
structuredCloneuses the structured clone algorithm, the same one used bypostMessageto 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.
- ▸
structuredClonedrops 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
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. Nowaandbare two names for one array:b.push(4)changes whatashows too, anda === bistrue. - 2
Comparing arrays with
===or==checks whether they are the same array, not whether they contain the same items:[1, 2] === [1, 2]isfalse. To compare contents, checklengthand each item (or for simple data, compareJSON.stringifyresults, with care). - 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, socopy[0].score = 99changesoriginal[0].scoretoo. For arrays of primitives (numbers, strings, booleans), a shallow copy is all you need. - 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 aDataCloneError), and class instances come back as plain objects. - 5
The old trick
JSON.parse(JSON.stringify(arr))also deep-copies plain data, but loses things:Datebecomes a string,MapandSetbecome{},undefinedand functions in arrays becomenull,NaNandInfinitybecomenull, and circular references throw. PreferstructuredClone. - 6
Often you don't need a deep copy at all: copy only the path you change. To update one item,
mapthe 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
What is the difference between a shallow copy and a deep copy of an array?
Why does [1, 2] === [1, 2] return false?
What can structuredClone copy that JSON.parse(JSON.stringify()) can't, and what can't it copy?
How do you update one object inside an array without mutating the original array?
Practice
Make a copy of const prices = [10, 20, 30]; that you can sort descending without changing prices, in two different ways.
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 = adoesn't copy an array.I can make a shallow copy four ways and know what it shares.
I can make a deep copy with
structuredCloneand know its limits.I know why the JSON trick loses data.
I can update one item in an array of objects without mutating anything.