Part 4 — Variables
var, let, const, Hoisting, The Temporal Dead Zone (TDZ), Scope, Reassignment vs mutation, immutability
var, let, const#
var 🔴 |
let 🟢 |
const 🟢 |
|
|---|---|---|---|
| Scope | function | block | block |
| Hoisted | yes, initialized to undefined |
yes, but in TDZ | yes, but in TDZ |
| Redeclarable in scope | yes | no | no |
| Reassignable | yes | yes | no |
| Creates global property (top level) | yes (window.x) |
no | no |
🟢 Default to const. Use let only when you must reassign. Never use var in new code.
Hoisting#
ELI12. Before running your code, JavaScript "reads ahead" and reserves a spot in memory for your declarations. For var, it also secretly writes undefined in that spot. For function declarations, it writes the whole function. So you can call a function before you wrote it in the file.
console.log(x); // undefined (var is hoisted & initialized)
var x = 5;
greet(); // works — function declarations are fully hoisted
function greet() { console.log("hi"); }
console.log(y); // ❌ ReferenceError — let is hoisted but in the TDZ
let y = 5;
sequenceDiagram
participant E as Engine
Note over E: Creation phase (hoisting)
E->>E: var x → memory, = undefined
E->>E: function greet → memory, = full function
E->>E: let y → memory, uninitialized (TDZ)
Note over E: Execution phase
E->>E: console.log(x) → undefined
E->>E: greet() → runs
E->>E: access y before init → ReferenceError
The Temporal Dead Zone (TDZ)#
The TDZ is the span from the start of a block until a let/const variable's declaration line. Accessing the variable there throws ReferenceError. It exists so that const can be guaranteed initialized before use and to catch use-before-declare bugs. 🎯
{
// TDZ for `a` starts here
// console.log(a); // ReferenceError
let a = 1; // TDZ ends
console.log(a); // 1
}
Scope#
graph TD
G[Global scope] --> F[Function scope]
F --> B[Block scope]
B -.lookup chain.-> F
F -.lookup chain.-> G
- Global — outermost. In browsers, top-level
var/functionattach towindow. - Function — each function call creates a new scope.
- Block —
{ }creates a scope forlet/const(notvar). - Lexical scoping — inner scopes can read outer variables; lookups walk outward through the scope chain. (Not "dynamic scoping" — where you are written matters, not who called you.)
⚠️ Classic loop trap (a favourite interview question 🎯):
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // 3, 3, 3 — one shared `i`
}
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // 0, 1, 2 — fresh binding per iteration
}
let creates a new binding each iteration, which is why closures capture the value you expect.
Reassignment vs mutation, immutability#
const prevents reassignment of the binding, not mutation of the value:
const arr = [1, 2];
arr.push(3); // ✅ fine — mutating the array
// arr = [4]; // ❌ TypeError — reassigning the binding
const obj = { a: 1 };
obj.a = 2; // ✅ fine
Object.freeze(obj); // now obj.a = 3 silently fails (throws in strict mode)
Object.freeze is shallow. For deep immutability you recurse, or use a library (Immer, immutable.js), or structuredClone + freeze.
Memory behaviour#
ELI12. Small simple values (numbers, strings, booleans) are stored directly in the variable's box. Big values (objects, arrays, functions) live somewhere else (the heap), and the variable box holds a ticket (reference) to them.
let a = 10;
let b = a; // b copies the value → 10
b = 20;
console.log(a); // 10 (unaffected)
let o1 = { n: 1 };
let o2 = o1; // o2 copies the reference (same object)
o2.n = 99;
console.log(o1.n); // 99 (both point to one object)
graph LR
subgraph Stack
a["a = 10"]
b["b = 20"]
o1["o1 = ref#1"]
o2["o2 = ref#1"]
end
subgraph Heap
obj["{ n: 99 }"]
end
o1 --> obj
o2 --> obj
This is the single most important idea behind "why did changing o2 also change o1?" — the answer is reference semantics. 🎯
Interview questions (Part 4):
- What is the TDZ and why does it exist?
- Predict the output of the
var/letloop above and explain. - Does
constmake an object immutable? What does it actually protect? - Explain hoisting differences between
var,let, function declarations, and function expressions.
Practice:
- Rewrite a
var-based loop that logs0..4asynchronously so it works, in three different ways (let, IIFE,.forEach). - Deep-freeze a nested object without a library.