Photo by chansu shin on Unsplash

On this page

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 here

const 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); // ReferenceError
let 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(); // 1
next(); // 2

Each 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

  1. 1. How does JavaScript decide which binding a name refers to inside a nested function?

Search articles

Type a word to search published articles.

Open the search page