JavaScript resolves names lexically: a function sees the bindings that existed in the source text where it was written, not where it is called. That rule, plus the fact that inner functions can outlive outer calls, is what people mean by closures.
Lexical scope and the scope chain
Each running function has an environment (often called a lexical environment) that holds local bindings and a link to its outer environment. Lookup walks that chain until a name matches or the chain ends.
const global = 1;
function outer() { const a = 2; function inner() { return a + global; } return inner;}inner closes over a and reaches global through the outer link. There is no separate “global scope object” for let/const at the top level in modules—they are module-scoped—but the idea of outer → inner chains is the same.
Block scope with let and const
let and const are block-scoped, like variables in C++ or Java inside { ... }. A binding exists from the start of its block until the block ends.
if (true) { let x = 1; const y = 2;}// x and y are not visible hereconst still allows mutating object contents; it forbids rebinding the identifier.
var and hoisting
var is function-scoped (or global at the top level), not block-scoped. Declarations are hoisted: the name exists for the whole function before the line runs, initialized to undefined until assignment.
function demo() { console.log(n); // undefined, not ReferenceError var n = 1; if (true) { var n = 2; } console.log(n); // 2 — same binding}If you are used to let in modern code, treat var as legacy: same function, one binding, no block boundary.
Temporal dead zone
let and const are hoisted in a sense, but accessing them before their declaration throws ReferenceError (the temporal dead zone). The binding exists but is uninitialized.
console.log(x); // ReferenceErrorlet x = 1;That matches the “declare before use” mental model from statically typed languages more closely than var does.
Closures
A closure is a function plus the environment it captured when created. When the outer function returns, inner functions still hold references to outer bindings, so those variables stay alive on the heap.
function makeCounter() { let count = 0; return () => ++count;}
const next = makeCounter();next(); // 1next(); // 2Each call to makeCounter() creates a new count binding. Closures are how callbacks, event handlers, and factories share hidden state without global variables.
Common closure pitfalls
Loops and var: A classic interview bug—var i in a loop shares one binding, so async callbacks often see the final value. Use let (per-iteration binding) or an IIFE to capture the current value.
Stale closures in React: A hook callback that closes over old props/state runs with outdated values unless dependencies are declared. The mechanism is the same closure rule; the fix is explicit dependency lists or functional updates.
Memory: Closures keep outer variables reachable. Holding a closure in a long-lived registry keeps the whole captured graph alive until the closure is dropped.
Next: The this keyword.
Check your understanding
Question 1 of 6
0 of 6 checked