Command Palette

Search for a command to run...

Hectal
PHASE 6Beginner ~26 min· topic 5 of 7

Topic 6.5

References and Mutation

In one line

A variable doesn't hold an object, it holds a reference (an address) to it, so assigning or passing an object shares the same one; === compares those references, Object.freeze locks an object (shallowly), and immutable updates make new objects instead of changing old ones.

Think of it like this

A house and its address. A number like 5 is written straight on your notepad: copy it to a friend's notepad and you each have your own 5. An object is a house; your notepad only holds its address. Give a friend the address and you both point at the same house. If they paint the door red, you'll see a red door too. Two houses that look identical are still two different houses at different addresses.

Words you'll meet

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

Primitive value
A simple value like a number, string, boolean, null or undefined. It is copied when assigned and can't be changed in place.
Reference
The "address" a variable holds for an object, telling the engine where the object is in memory.
Heap
The area of memory where objects live.
Mutation
Changing an existing object in place, like setting a property or pushing to an array.
Identity
Whether two references point at the very same object. === checks identity for objects.
Object.freeze
Locks an object so its properties can't be added, removed or changed (one level deep).
Strict mode
A stricter version of JavaScript (on in modules and classes, or with "use strict") that turns silent failures into errors.
Deep copy
A copy where nested objects are copied too, so nothing is shared with the original.
Immutable update
Making a new object with the change instead of changing the old one.

Step by step

01Primitives are copied

Changing b doesn't touch a: each variable has its own number.

app.jswhole filejavascript
let a = 5;
let b = a;
b = 10;
console.log(a, b); // 5 10

02Objects are shared

p2 = p1 copies the address. There is still only one object, so a change through p2 shows through p1.

app.jswhole filejavascript
const p1 = { score: 5 };
const p2 = p1;
p2.score = 10;
console.log(p1.score); // 10
Objects are shareddiagram
Rendering diagram…

03Equal-looking is not the same

Two literals create two objects. === asks "same object?", so the answer is false even though they look identical.

app.jswhole filejavascript
console.log({ score: 10 } === { score: 10 }); // false
console.log(p1 === p2);                      // true

04Functions receive the reference

A function that mutates its argument changes the caller's object. Prefer functions that return a new object, so callers decide what to keep.

app.jswhole filejavascript
function addBonus(player) {
  return { ...player, score: player.score + 50 }; // new object
}

05Update nested data immutably

Copy each level on the path to the change, and share everything else. The untouched parts (cart here) are the same objects as before, which saves memory and makes "what changed?" a quick !== check.

app.jswhole filejavascript
const state = { user: { name: "Asha", prefs: { theme: "light" } }, cart: [] };
const next = {
  ...state,
  user: { ...state.user, prefs: { ...state.user.prefs, theme: "dark" } },
};

Try it yourself

  1. 1

    Copy before changing

    In the first example, change const p2 = p1; to const p2 = { ...p1 };. Predict the second, third and last lines.

  2. 2

    Freeze deeply

    In the third example, wrap the inner object too: limits: Object.freeze({ perMinute: 60 }). Predict the first two lines.

  3. 3

    Clone a Map

    In the fourth example, add tags: new Map([["a", 1]]) to order and log deep.tags. Predict what structuredClone gives you.

Code & diagrams

Two names, one object New tab

Putting `p1` into an array also stores a reference, so changing `list[0]` changes `p1`.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

5 10
10 10
true
false
99
Functions that mutate vs functions that don't New tab

`addBonus` changed the caller's object. `replace` only pointed its own parameter at a new object, which never affects the caller's variable.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

100 150 false
150
Asha
Object.freeze is shallow, and quiet unless strict New tab

In a normal script the first two writes were ignored without a word. Inside a `"use strict"` function the same write throws. The nested `limits` object wasn't frozen, so it changed.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

3 undefined 1000
true false
TypeError: Cannot assign to read only property 'retries' of object '#<Object>'
Shallow copy vs structuredClone New tab

The shallow copy shares `items` with the original; the deep copy has its own. `structuredClone` keeps the `Date` as a real date, but refuses functions.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

99 2
true 2024-01-15T10:30:00.000Z
DataCloneError
Immutable nested update ES2018+ New tab

Every object on the path to `theme` is new; `cart` wasn't touched, so it's shared. A quick `!==` tells you exactly which parts changed.

Preview

Press Run to see the page.

Console

Nothing logged yet.

Expected console output

light dark
false false true

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" by assignment

Write const draft = settings; draft.theme = "dark"; intending to keep settings unchanged.

terminal
$ Console
── what you'll see ──
settings.theme is now 'dark' too.

Break #2

Change a frozen object in strict code

Add "use strict"; as the first line of the third example's JavaScript.

terminal
$ Console
── what you'll see ──
Uncaught TypeError: Cannot assign to read only property 'retries' of object '#<Object>'

Myth vs fact

Myth

JavaScript passes objects by reference.

Fact

It passes a copy of the reference (sometimes called "call by sharing"). The function can mutate the object, but assigning a new object to the parameter doesn't change the caller's variable.

Myth

const obj = {...} makes the object constant.

Fact

const makes the binding constant: obj can't point elsewhere. The object's contents can still change. Use Object.freeze to lock contents.

Myth

Object.freeze makes an object immutable all the way down.

Fact

Only the top level. Nested objects stay mutable unless you freeze them too (a "deep freeze" function freezes each level).

Myth

Two objects with the same properties are equal.

Fact

== and === both compare identity for objects. Equal contents need a property-by-property comparison.

Pro corner

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

  • ▸

    Objects are freed by the garbage collector when nothing can reach them any more. Holding references in long-lived places (global arrays, caches, event listeners) keeps objects alive: that's how memory leaks happen in JavaScript. WeakMap and WeakRef (Phase 14.4) hold references that don't keep objects alive.

  • ▸

    Object.freeze changes each property's descriptor to writable: false, configurable: false and makes the object non-extensible (Topic 6.7 shows descriptors). Frozen objects can still be optimised by engines, and freezing in development builds is a cheap way to catch accidental mutation.

  • ▸

    structuredClone (a browser and Node 17+ API, not part of ECMAScript) uses the same algorithm as postMessage to workers. It keeps shared and circular references intact, drops prototypes (class instances come back as plain objects), and throws DataCloneError for functions, DOM nodes and symbols.

  • ▸

    Libraries such as Immer let you write "mutating" code against a draft and produce an immutable update with structural sharing, which keeps deeply nested updates readable.

Remember this

  1. 1

    Primitive values (numbers, strings, booleans, null, undefined, bigints, symbols) are copied when you assign them: let b = a gives b its own copy. They are also immutable: you can't change the number 5 or the string "hi" in place, only make new values. Objects (including arrays and functions) live in a part of memory called the heap, and a variable holds a reference to the object, meaning where to find it.

  2. 2

    So const p2 = p1 copies the reference, not the object. Both names now point at one object, and a change through either name (mutation: changing an object in place, like p2.score = 10 or arr.push(1)) is visible through both. The same happens when you pass an object to a function: the function receives the reference and can mutate the caller's object. (Strictly, JavaScript passes everything by value; for objects, that value is the reference. Reassigning the parameter inside the function doesn't affect the caller.)

  3. 3

    === on objects compares identity: "is this the same object?", not "do they look the same?". { a: 1 } === { a: 1 } is false (two objects), while p1 === p2 is true when both point at one object. To compare contents you check the properties yourself (for flat objects, compare keys and values), or use a library function. JSON.stringify(a) === JSON.stringify(b) works only for simple data with keys in the same order.

  4. 4

    const doesn't stop mutation: it only stops the variable from pointing somewhere else. To lock the object itself, use **Object.freeze(obj): no adding, deleting or changing properties. In normal ("sloppy") scripts, an attempt to change a frozen object fails silently; in strict mode** (modules, classes, or code under "use strict") it throws a TypeError. Freeze is shallow: nested objects stay changeable unless you freeze them too. Softer locks exist: Object.seal (no adding or deleting, changing allowed) and Object.preventExtensions (no adding).

  5. 5

    A shallow copy ({ ...obj }, Object.assign({}, obj), [...arr]) duplicates only the top level. A deep copy duplicates everything inside too. The browser's built-in **structuredClone(value)** makes deep copies and handles Date, Map, Set, nested arrays and even circular references; it throws for functions and DOM nodes. JSON.parse(JSON.stringify(x)) is an older trick that loses dates, undefined, Map and more (Topic 6.6). Phase 5.10 covers the same ideas for arrays.

  6. 6

    An immutable update means: never change the existing object; build a new one with the change. const next = { ...state, count: state.count + 1 }. For nested data, copy every level along the path you change and share the rest: { ...state, user: { ...state.user, name: "Ravi" } }. Frameworks such as React rely on this: they detect changes by comparing references (old !== new), which is instant, instead of comparing every property. Mutating in place keeps the same reference, so the change can go unnoticed.

Explain it without notes

01

What is the difference between how primitives and objects are assigned and passed to functions?

02

Why is { a: 1 } === { a: 1 } false? How would you compare two objects by content?

03

What does Object.freeze do, and what are its limits?

04

What is an immutable update and why do UI frameworks rely on it?

05

When would you use structuredClone instead of spread?

Practice

01

Write shallowEqual(a, b) that returns true when two flat objects have the same keys and the same values (===). Test it with { x: 1, y: 2 } vs { y: 2, x: 1 } and vs { x: 1 }.

02

Write deepFreeze(obj) that freezes an object and every nested object inside it. Show that a nested change no longer works.

03

Given const todo = { id: 1, text: "Read", tags: ["home"] }, make updated, which adds the tag "urgent", without changing todo. Log both tag lists.

Trade-offs

  • ↔

    Mutation is fast and uses no extra memory, but shared objects can change behind your back; immutable updates are safer and easy to compare, but create more objects for the garbage collector.

  • ↔

    structuredClone is a correct deep copy for data, but it's slower than a targeted spread of the parts you change and loses class identity; copy only what you need.

  • ↔

    Object.freeze catches accidental writes, but silently in sloppy scripts; it's most useful together with strict mode (modules) or in development builds.

Done when you can

  • I can explain the difference between a value and a reference with a picture.

  • I can predict when a change through one variable shows through another.

  • I know === compares objects by identity.

  • I can freeze an object and know freeze is shallow and silent outside strict mode.

  • I can choose between spread and structuredClone for copying.

  • I can write an immutable update for nested data.