Course outline · 0% complete

0/27 lessons0%

Course overview →

Copies vs references, and the final gauntlet

lesson 10-3 · ~12 min · 27/27

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.

variablesn = 5a ●b ●one shared object{ items: [1, 2] }n owns its value. a and b hold arrows to the SAME object.
Assignment copies what the variable holds: a value for primitives, an arrow for objects. b = a duplicates the arrow, not the object.

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.

UnitThe one-line answer
1closures capture variables, not values
2this is decided by the call, not the definition
3methods live once on the prototype
4microtasks drain before the next macrotask
5promises chain, and rejections fall downward
6Promise.all runs independent work in parallel
7generators pause and resume, lazily
8spread copies one level
9regex needs anchors to validate
10var 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

4

The 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.