JavaScript scope guide
How the JavaScript Scope Chain Works
Understand lexical scope, identifier lookup, shadowing, and closures through small JavaScript traces you can run.
The phrase scope chain describes how JavaScript searches for an identifier when the current scope does not contain a matching binding. The important word is lexical: the chain is determined by where code is written.
This guide separates the useful mental model from a common mistake. JavaScript does not search the call stack for a variable. It follows the outer lexical environments associated with the current code.
The short answer
- A binding belongs to the lexical scope in which it is declared.
- Lookup starts in the nearest environment and continues outward until a binding is found or the outer chain ends.
- An inner declaration with the same name shadows an outer declaration for code inside that inner scope.
1. Scope is fixed by source structure
A function carries an association with the lexical environment in which it was created. This is why a nested function can read bindings from its surrounding function, even when it is called from somewhere else.
The call stack still matters for the order in which code runs. It does not decide which outer bindings are visible to a function body.
2. Lookup starts at the closest binding
The inner function reads label. Its own environment has no label binding, so lookup continues to the environment of outer, where label is found. This nearest-first rule is what makes local variables useful and predictable.
label resolves in outer because inner does not declare it
function outer() {
const label = "outer";
function inner() {
console.log(label);
}
inner();
}
outer();3. Shadowing changes only the inner view
When inner declares another label, that binding is found first. The outer binding still exists; it is simply not the one selected for an unqualified label reference inside inner.
Shadowing is not mutation. Assigning inner label changes the inner binding unless the code explicitly reaches some object or outer binding by another route.
The inner label shadows the outer label
const label = "global";
function outer() {
const label = "outer";
function inner() {
const label = "inner";
console.log(label);
}
inner();
}
outer();4. Closures retain the lexical connection
A closure is not a copy of every value from an outer function. It is the situation in which a function can continue to access bindings from its lexical environment after the surrounding call has returned.
In this example, increment keeps access to count. Each call resolves count through the environment connected to the function’s creation site, then updates that binding.
The returned function still resolves count in makeCounter’s environment
function makeCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const next = makeCounter();
console.log(next());5. A reliable way to debug a name
When a variable reference surprises you, write down the current function or block first. Then ask: is the name declared here? If not, move one lexical layer outward and repeat. Stop at the global environment; if no binding exists, the reference fails.
The Scope Chain visualizer makes that search explicit. Select a nested example, predict the resolution path, and use the highlighted lookup to check your reasoning.