Frontend Labs
⋯

4. Variables and scope: var/let/const, hoisting, the TDZ

var a = 1;    // function-scoped, hoisted, re-declarable — avoid in new code
let b = 2;    // block-scoped, hoisted into a "temporal dead zone", not re-declarable
const c = 3;  // block-scoped like let, binding cannot be reassigned (contents still mutable)

const arr = [1, 2];
arr.push(3);   // fine — the array's contents mutate
arr = [];      // TypeError — the binding `arr` is const

Hoisting: declarations are processed before code runs, but var and let/const behave differently:

console.log(x);  // undefined (not an error) — var is hoisted AND initialized to undefined
var x = 5;

console.log(y);  // ReferenceError: Cannot access 'y' before initialization
let y = 5;        // let/const are hoisted but NOT initialized — this gap is the
                   // "Temporal Dead Zone" (TDZ), and it's a deliberate safety net

The classic var-in-a-loop closure bug, and why let fixed it:

// var: all three callbacks share ONE function-scoped `i`, which is 3 by the time they run
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);   // logs 3, 3, 3
}

// let: each iteration gets its OWN block-scoped binding of `i`
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);   // logs 0, 1, 2
}

Closures: a function "closes over" the variables in its surrounding scope, keeping them alive even after that scope has returned — this is the mechanism behind the let example above, and behind every module pattern, memoizer, and event handler that references outer state.

function makeCounter() {
  let count = 0;               // captured by the closure below
  return function increment() {
    count += 1;
    return count;
  };
}
const counter = makeCounter();
counter(); // 1
counter(); // 2 — `count` persisted between calls, private to this closure

Generated from real-life-JS-guide.md · 8 Oct 2026 16:36