Base CS from zero
References vs values
You have seen that assignment writes a value into a cell. But what exactly is the “value”
that gets written? For a number like 42, the answer is obvious: the bit pattern for 42.
Copy it from one cell to another, and each cell independently holds 42.
Now consider an object: { x: 1, y: 2 }. An object can be large — many fields, nested
structure. The runtime does not copy all of that into a single cell every time you
assign. Instead, it stores a compact reference — essentially an address pointing at
where the object lives. When you assign that variable to another, both variables end up
holding the same reference, pointing at the same object. This is called aliasing.
The consequence: mutating the object through one variable name is immediately visible through the other. A single assignment line can cause surprising behaviour if you do not understand that two names are sharing one thing.
This distinction — primitive values are copied, objects are shared by reference — is one of the most important things to understand about how JavaScript and TypeScript work.
After this lesson you can explain why assigning a primitive copies the value while assigning an object copies only the reference, predict the result of aliasing a variable to an object and then mutating through the alias, and trace a listing that mixes both primitive and object assignments.
What sits in the cell: value or reference?
You already know that a variable is a named memory cell. The cell holds some bit pattern. For JavaScript/TypeScript the critical question is: what bit pattern does the cell hold when you assign a primitive vs when you assign an object?
An object is a compound value made up of named fields (also called properties): for
example, { x: 1, y: 2 } is an object with two fields, x and y, each holding its
own value. Unlike a primitive such as a number, an object can grow arbitrarily large and
is never packed into a single memory cell.
Primitive values — copy semantics. A primitive value (number, string, boolean, null,
undefined) is small enough to fit directly in the cell. When you assign a primitive to
a variable, the runtime writes the actual value bits into the cell. When you then copy
that variable to another (let b = a), the runtime copies the bit pattern from one cell
to the other. Both cells now hold independent copies of the value. Changing one does not
affect the other.
let a = 42;
let b = a; // b's cell gets a copy of 42
a = 99; // a's cell changes; b's cell still has 42Object and array values — reference semantics. An object ({ x: 1 }) or array
([1, 2, 3]) is a compound structure that lives at some address in a separate region
of memory called the heap. It can be large and is not packed into a single cell.
Instead, the variable’s cell holds a reference — a number that is the address of
the object on the heap. This is sometimes called a “pointer” in lower-level languages.
When you assign an object to a second variable (let b = a), the reference (the address
number) is copied — not the object itself. Both a and b now hold the same address
in their cells. They both point to the same object on the heap. This is called
aliasing: two names for one thing.
Aliasing means: mutate through one, see the change through the other. Because a
and b point at the same object, adding a field via a.z = 5 means b.z is also 5 —
there is only one object.
const and object references. const obj = { x: 1 } means the variable obj
cannot be rebound — you cannot write obj = somethingElse. But the object that obj
points to is not frozen: obj.x = 99 is perfectly valid. const protects the binding
(which reference the cell holds), not the contents of the object at that reference.
1
// --- PRIMITIVE: copy semantics ---
2
let a = 42;
3
let b = a; // b's cell gets a copy of the bit pattern 42
4
a = 99; // a's cell now holds 99; b's cell still holds 42
5
console.log(b); // 42 — independent copy, unaffected by changing a
6
7
// --- OBJECT: reference semantics ---
8
let p = { x: 1, y: 2 }; // object lives on the heap at (say) addr 5000
9
let q = p; // q's cell gets the same reference: addr 5000
10
// p and q now ALIAS the same object
11
q.x = 99; // mutate the object via q
12
console.log(p.x); // 99 — same object, same mutation visible via p
13
14
// --- const and reference ---
15
const obj = { val: 10 }; // obj's cell holds the reference; binding is sealed
16
// obj = { val: 20 }; // TypeError: cannot rebind the name
17
obj.val = 20; // OK: the object itself is not frozen
18
console.log(obj.val); // 20
- L2 a's cell: bit pattern for 42 (primitive)
- L3 b's cell gets a copy of those bits. Two independent cells, both holding 42.
- L4 a's cell changes to 99. b's cell is unaffected — it has its own copy.
- L8 Object allocated on the heap. p's cell holds the heap address (reference).
- L9 q's cell gets a copy of the reference (same address). Both point at the same object.
- L11 Mutation via q: changes the object at addr 5000. Both p and q still point there.
- L12 p.x is 99 — the same object was mutated. Aliasing made the change visible via p.
- L15 const seals the binding: obj's cell always holds the same reference.
- L16 Rebinding fails: the cell cannot be overwritten with a different reference.
- L17 Mutating the object's property is fine: const only guards the cell's reference, not the object's contents.
Trace aliasing: p and q share one object. Watch how a mutation through q appears
through p.
1
let p = { x: 1 };
2
let q = p;
3
q.x = 99;
4
console.log(p.x);
Common mistake
A very common mistake: assuming that let q = p creates a copy of the object. It does
not. It creates a copy of the reference — a copy of the address number. Afterwards,
both p and q hold the same address, and there is still only one object on the heap.
To create an independent copy of an object, you must explicitly copy it:
let q = { ...p } (spread syntax creates a shallow copy). Even then, nested objects
inside are still shared — this is the difference between shallow and deep copies, a
topic for a later lesson.
Edge cases
Strings are primitive in JavaScript (they are not objects), but they are also not fixed- size the way numbers are. Strings are immutable: every “mutation” of a string actually produces a new string value, and the variable is updated to hold the new string value. You cannot change a character inside a string in place the way you can change a field inside an object. This is why strings appear to behave like primitives (each variable gets an independent copy when you assign) even though strings are internally stored as structured values. The TC39 spec classifies string as a primitive type.
let a = 10; let b = a; a = 99; What is b?
let x = { n: 5 }; let y = x; y.n = 42; What is x.n?
let p = { v: 1 }; let q = { v: 1 }; q.v = 7; What is p.v?
let a = [1, 2, 3]; let b = a; b[0] = 99; What is a[0]?
const c = { x: 0 }; c.x = 5; What is c.x?
Why does mutating an object via one variable name also change what another variable sees?
When you assign a primitive value (number, string, boolean, etc.), the actual bit
pattern is copied into the destination cell. Each variable holds an independent copy;
changing one does not affect the other. When you assign an object or array, what
is copied is the reference — the heap address of the object — not the object itself.
Two variables holding the same reference are aliases: they share one object.
Mutating that object through either name is immediately visible through the other.
const seals the binding — the reference in the cell cannot be replaced — but does not
freeze the object’s contents. Strings are primitives and behave with copy semantics even
though they are not fixed-size.