Topics in this subject
JavaScript 4 min read Updated 4 Aug 2026

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/function attach to window.
  • Function — each function call creates a new scope.
  • Block{ } creates a scope for let/const (not var).
  • 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):

  1. What is the TDZ and why does it exist?
  2. Predict the output of the var/let loop above and explain.
  3. Does const make an object immutable? What does it actually protect?
  4. Explain hoisting differences between var, let, function declarations, and function expressions.

Practice:

  • Rewrite a var-based loop that logs 0..4 asynchronously so it works, in three different ways (let, IIFE, .forEach).
  • Deep-freeze a nested object without a library.