Scope, Hoisting & Closures

Intermediate · 20 min read · ▶ live playground · ✦ checkpoint

Where does a variable live, and who's allowed to see it? That question — scope — sounds like bureaucracy, but its answer contains the single most powerful idea in JavaScript: functions that remember. This is the lesson where var's old ghosts finally make sense, and where you earn a tool professionals use every single day without thinking.

Where a name lives: three kinds of scope

Every variable is visible only inside the region where it was declared — its scope. JavaScript has three:

  • Global scope — declared outside everything; visible everywhere. Convenient, and dangerous in the same way a shared whiteboard is: anyone can overwrite it.
  • Function scope — declared inside a function; invisible outside it.
  • Block scope — declared with let/const inside any { … } block (an if, a loop, or just braces); invisible outside that block.
const stadium = "Motera";        // global — everyone sees it
 
function announce() {
  const speaker = "Ada";         // function scope — only announce() sees it
  if (true) {
    const volume = 11;           // block scope — only this if-block sees it
    console.log(stadium, speaker, volume); // inner code sees ALL outer names
  }
}
announce();

Motera Ada 11 — code can always look outward. The reverse is not true: outside the function, speaker doesn't exist.

Lookups walk the scope chain: the engine checks the current block, then the enclosing function, then outward until global. First match wins; no match anywhere → ReferenceError. Inner scopes can see out; outer scopes can never see in.

Hoisting for real: var vs. let/const

Before running your code, the engine registers every declaration it will contain — that pre-registration is called hoisting. What differs is the state each keyword's variable is in before its line runs:

console.log(score);  // no crash!
var score = 10;

undefined — the var name exists from the top of the function, value not assigned yet. A silent almost-bug: real code that "works" while quietly reading nothing.

let and const are also hoisted — but locked. The zone between the top of the scope and the declaration line is the temporal dead zone (TDZ), and touching the name there is a crash:

console.log(score);  // TDZ — the name exists but is untouchable
let score = 10;

Uncaught ReferenceError: Cannot access 'score' before initialization — loud, immediate, and pointing at the exact line. This is let fixing var's silent-undefined trap, not adding a new one.

Add the fact that var ignores block boundaries entirely (a var inside an if leaks out into the whole function) and you have the complete case for the rule from lesson 2.1: write let/const, read var as history.

Closures: functions remember where they were born

Here's the payoff. Watch closely — a function is about to outlive the scope it was created in, and keep using it:

function makeGreeter(name) {
  const greeting = `Hello, ${name}!`;   // lives in makeGreeter's scope
  return function () {                   // born inside that scope...
    console.log(greeting);
  };
}
 
const greetAda = makeGreeter("Ada");
greetAda();   // makeGreeter finished long ago — and yet:

Hello, Ada! — the inner function still reads greeting, after makeGreeter has already returned.

By the "variables die with their scope" intuition, greeting should be gone. It isn't, because of the deal JavaScript makes with every function:

Try it — private state, no object required

makeCounter gives every counter its own private count that nothing else in the program can touch — there is no way to reach it except calling the function. Run it, then type tally() a few times at the prompt and watch the memory persist.

javascript — live consolereal engine
⌘/Ctrl + Enter to run

Two counters, two birthplaces, two independent memories. This pattern — function factories with private state — is how debounced search boxes, rate limiters, and half the React ecosystem work under the hood.

The classic loop bug (and why let fixed it)

For twenty years, this was the JavaScript interview question. setTimeout(fn, ms) runs a function after a delay (the full story is in lesson 10.2 — here it just means "later"). Predict the output, then run it:

javascript — live consolereal engine
⌘/Ctrl + Enter to run

The output: var i: 3 three times, then let j: 0, 1, 2. Same loop, same callbacks — the only difference is how many birthplaces exist. var makes one shared variable; let mints a new one per iteration, so each closure remembers its own. When this clicks, you understand closures.

Closures are the engine behind the next lesson too — higher-order functions, where functions take and return other functions like any other value.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion