What assignment actually copies
Primitives, meaning numbers, strings, booleans, null, undefined, symbols, and bigints, are copied by value. Two variables hold two independent values.
Objects, including arrays and functions, are copied by reference.
The variable does not contain the object. It contains a reference, which is the object's location in memory, and assignment copies that reference, so both variables end up naming one shared object.
If it helps, picture the reference as an arrow. b = a copies the arrow, not the thing it points at.
Three consequences get probed in interviews.
const only locks the reference, so const arr = [] can still be pushed to. Spread copies one level only, so nested objects stay shared, which is lesson 8-2's shallow-copy warning. structuredClone(obj) makes a true deep copy.
Function arguments follow the same rule, which is where it hurts most. Passing an object into a function hands over the arrow, so the function can mutate the caller's data.
This distinction sits behind a huge share of real bugs, including the function that mysteriously edits its caller's data and the UI state that never re-renders because the reference never changed.
Three kinds of copy
One object, three ways of duplicating it.
const a = { items: [1] }; const b = a; b.items.push(2); console.log(a.items.length); const shallow = { ...a }; console.log(shallow.items === a.items); const deep = structuredClone(a); console.log(deep.items === a.items); deep.items.push(3); console.log(a.items.length);
Output
2 true false 2
b and a share one object, so pushing through b is visible through a.
The spread copy shares the nested array, which the true on the second line proves. The outer object is new and the inner array is the same one.
Only structuredClone is fully independent, so pushing to deep.items leaves a.items at length 2.
const b = a still allowed the mutation, which is the const point restated. It locks the binding rather than the contents.
structuredClone is built into modern browsers and Node, and it has limits worth knowing. It cannot clone functions, DOM nodes, or class identity, so a cloned instance comes back as a plain object.
JSON.parse(JSON.stringify(obj)) is the old workaround, and it silently drops undefined, functions, and symbols while turning Date objects into strings.
The line that throws is team = [].
const freezes the binding, which is the arrow, and not the object it points at.
So team.push("Grace"), team[0] = "Grace", and team.length = 0 are all fine, because they mutate through the arrow rather than replacing it.
Reassigning the variable to a new array is what const forbids, and it throws a TypeError saying assignment to constant variable.
The name is genuinely misleading, and reading const as "constant binding" rather than "constant value" resolves most of the confusion.
Object.freeze is the tool for actual immutability, and it stops property writes on the object itself. It is also shallow, so Object.freeze(obj) leaves obj.nested fully mutable.
In sloppy mode a write to a frozen object fails silently, and in strict mode it throws, which is another reason module code being strict is a good thing.
withDiscount
The original version mutated its argument.
function withDiscount(order, percent) { const copy = { ...order }; copy.price = copy.price * (1 - percent / 100); return copy; } const order = { id: 7, price: 100 }; const discounted = withDiscount(order, 20); console.log(discounted.price); console.log(order.price);
Output
80 100
Writing const copy = order makes both names point at one object, so the mutation hits the original.
Building a new object with const copy = { ...order } gives the function something of its own to mutate freely.
Spread is enough here because order is one level deep. An order with a nested customer object would need a deeper copy or a nested spread.
The const on copy is not what protects the caller. It prevents rebinding copy, and the spread is what created a separate object.
A function that returns a new value instead of editing its input is called pure in this respect, and it is the reason this style dominates React state updates and reducers.
Note the price arithmetic inherits lesson 10-2's float issue, so a 33% discount on 100 gives 67.00000000000001. Money belongs in integer cents.
You made it
Here is the interview map you now carry.
| Unit | The one-line answer |
|---|---|
| 1 | closures capture variables, not values |
| 2 | this is decided by the call, not the definition |
| 3 | methods live once on the prototype |
| 4 | microtasks drain before the next macrotask |
| 5 | promises chain, and rejections fall downward |
| 6 | Promise.all runs independent work in parallel |
| 7 | generators pause and resume, lazily |
| 8 | spread copies one level |
| 9 | regex needs anchors to validate |
| 10 | var hoists to undefined, objects assign by reference |
Each row is a sentence you can say in an interview and then defend with an example, which is the actual skill being tested.
The deeper pattern is worth naming as well. Almost every answer above comes from asking where something was written or how it was called, and those two questions carry most of JavaScript's surprises.
One last classic sits below. Say your reasoning out loud, exactly as you should in the real interview.
Final gauntlet
One number.
const arr = [1, 2, 3]; const copy = arr; copy.push(4); console.log(arr.length);
Output
4The answer is 4.
copy = arr duplicates the reference, so both variables point at one array, and the name copy is a lie.
The push through copy is visible through arr too, which is why the length is 4 rather than 3.
To get an independent copy you would spread it as const copy = [...arr], and arr.slice() does the same job.
The spread is still shallow, so an array of objects shares those objects. [...rows] gives a new array whose entries are the same rows.
That is the note to end on, because it ties the whole unit together. Copying in JavaScript is always a question of how many levels, and the answer is one unless you asked for more.