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,
nullorundefined. 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.
let a = 5;
let b = a;
b = 10;
console.log(a, b); // 5 1002Objects are shared
p2 = p1 copies the address. There is still only one object, so a change through p2 shows through p1.
const p1 = { score: 5 };
const p2 = p1;
p2.score = 10;
console.log(p1.score); // 1003Equal-looking is not the same
Two literals create two objects. === asks "same object?", so the answer is false even though they look identical.
console.log({ score: 10 } === { score: 10 }); // false
console.log(p1 === p2); // true04Functions 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.
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.
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
Copy before changing
In the first example, change
const p2 = p1;toconst p2 = { ...p1 };. Predict the second, third and last lines. - 2
Freeze deeply
In the third example, wrap the inner object too:
limits: Object.freeze({ perMinute: 60 }). Predict the first two lines. - 3
Clone a Map
In the fourth example, add
tags: new Map([["a", 1]])toorderand logdeep.tags. Predict whatstructuredClonegives you.
Code & diagrams
Putting `p1` into an array also stores a reference, so changing `list[0]` changes `p1`.
Preview
Console
Nothing logged yet.Expected console output
5 10
10 10
true
false
99`addBonus` changed the caller's object. `replace` only pointed its own parameter at a new object, which never affects the caller's variable.
Preview
Console
Nothing logged yet.Expected console output
100 150 false
150
AshaIn 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
Console
Nothing logged yet.Expected console output
3 undefined 1000
true false
TypeError: Cannot assign to read only property 'retries' of object '#<Object>'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
Console
Nothing logged yet.Expected console output
99 2
true 2024-01-15T10:30:00.000Z
DataCloneErrorEvery 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
Console
Nothing logged yet.Expected console output
light dark
false false trueBreak 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.
Break #2
Change a frozen object in strict code
Add "use strict"; as the first line of the third example's JavaScript.
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.
WeakMapandWeakRef(Phase 14.4) hold references that don't keep objects alive. - ▸
Object.freezechanges each property's descriptor towritable: false, configurable: falseand 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 aspostMessageto workers. It keeps shared and circular references intact, drops prototypes (class instances come back as plain objects), and throwsDataCloneErrorfor 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
Primitive values (numbers, strings, booleans,
null,undefined, bigints, symbols) are copied when you assign them:let b = agivesbits 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
So
const p2 = p1copies the reference, not the object. Both names now point at one object, and a change through either name (mutation: changing an object in place, likep2.score = 10orarr.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
===on objects compares identity: "is this the same object?", not "do they look the same?".{ a: 1 } === { a: 1 }isfalse(two objects), whilep1 === p2istruewhen 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
constdoesn'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) andObject.preventExtensions(no adding). - 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 handlesDate,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,Mapand more (Topic 6.6). Phase 5.10 covers the same ideas for arrays. - 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
What is the difference between how primitives and objects are assigned and passed to functions?
Why is { a: 1 } === { a: 1 } false? How would you compare two objects by content?
What does Object.freeze do, and what are its limits?
What is an immutable update and why do UI frameworks rely on it?
When would you use structuredClone instead of spread?
Practice
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 }.
Write deepFreeze(obj) that freezes an object and every nested object inside it. Show that a nested change no longer works.
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.
- ↔
structuredCloneis 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.freezecatches 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
structuredClonefor copying.I can write an immutable update for nested data.