JavaScript Scope, Environments, and Closures
Learn how JavaScript resolves names through lexical scope, how environments nest, and why closures preserve access to surrounding bindings.
Intermediate
Scope answers a deceptively simple question: when JavaScript sees a name such as
count, which binding does that name refer to? The answer comes from lexical scope. Once you understand that model, closures stop looking like magic: a function can keep access to the surrounding lexical environment even after the surrounding function has returned.
1. Scope is about where a name can be resolved
A binding is a name associated with a value. A lexical scope is the region of source code in which that binding can be resolved by name.
let and
const create lexical bindings, and blocks and functions create nested scopes. JavaScript resolves an identifier by looking in the current environment and then following its outer environments when necessary.
Output
Ready to run.
Quick check
Which binding does the return statement read?
function showMessage() {
const message = "function";
return ;
}
2. Shadowing: same name, different binding
A nested scope may declare a binding with the same name as an outer binding. The inner binding shadows the outer one for code inside the inner scope. Shadowing does not change the outer binding; it changes which binding an identifier reference resolves to.
Output
Ready to run.
Quick check
After inspect() returns, what does the outer status contain?
let status = "outer";
function inspect() {
let status = "inner";
}
console.log(status === true
3. Functions create nested lexical environments
Each function invocation gets its own function environment for its local bindings. A nested function can resolve names from its surrounding function because its lexical environment has an outer environment. This is lexical scope: the source nesting determines the lookup path, not the call site.
Output
Ready to run.
Quick check
Where does read() find label?
function makeReader() {
const label = "enclosed";
function read() { return ;
4. Closures preserve access to outer bindings
A closure is a function together with the lexical environment needed to resolve the names it uses. In practical terms, a function returned from another function can continue to use the surrounding bindings after the outer function has finished. The binding is not copied into a string snapshot: if the closed-over binding changes, later calls can observe the new value.
Output
Ready to run.
Quick check
What does the third call to next() return?
let count = 2;
count += 1;
console.log(count
5. Closures capture bindings, not a frozen value
The important detail is the binding itself. Multiple closures can share an outer binding, so they can observe and modify the same state. This is useful for private state, factories, event handlers, and callbacks.
Output
Ready to run.
Quick check
After increment() runs once, what does read() observe?
const pair = makePair();
pair.increment();
console.log(pair.read() === 1
6. Block scope is real at runtime
A block can introduce a lexical environment of its own. This matters in branches and loops: a binding declared with
let or
const inside a block is not available after that block. Each iteration of a loop can also provide distinct lexical bindings for iteration variables, which is important when closures are created inside the loop.
Output
Ready to run.
Quick check
Why does the loop produce 0, 1, 2 instead of three 3s?
const readers = [];
for (let i = 0; i < 3; i += 1) {
readers.push(() => i);
}
console.log(readers.map((read) => read()).join(",") === "0,1,2"
7. Closures explain callbacks
Callbacks are ordinary functions passed somewhere to be called later. When a callback reads a surrounding binding, it can close over that binding. The callback does not need the caller to pass every piece of state explicitly. This pattern appears throughout JavaScript, including browser event handlers and array methods.
Output
Ready to run.
Quick check
What value is formatUser closing over?
function makeFormatter(prefix) {
return (name) => `${prefix}: ${name}`;
}
console.log(makeFormatter("User")("Ada") === "User: Ada"
8. Lexical scope is not dynamic scope
JavaScript uses lexical scope: the meaning of a name is determined by where code is written. Calling a function from a different scope does not make that caller’s local variables visible to the called function. This is one of the most important debugging rules for nested functions.
Output
Ready to run.
Quick check
What does callWithLocal() return?
function readName() { return name; }
const name = "global";
console.log(readName() === "global"
9. A practical mental model for name lookup
For learner-level debugging, use this lookup sequence: start where the identifier is used; if that scope has the binding, use it; otherwise move outward through the lexical nesting chain; continue until a binding is found or the chain ends. If the chain ends without a binding, the identifier is unresolved and evaluation produces a
ReferenceError. The specification calls the underlying structures Environment Records and uses outer-environment references to model this process.
Output
Ready to run.
Quick check
Which lookup order allows inner() to produce ABC?
function outer() {
const page = "B";
function inner() {
const button = "C";
return `${app}${page}${button}`;
}
}
const app = "A";
console.log(outer() === "ABC"
10. Avoid confusing scope with object properties
A lexical binding is not the same thing as an object property.
user and
user.name participate in different mechanisms: the first resolves the binding named user, while the second first resolves user and then performs property access on its value. This distinction becomes important when you move from lexical scope to objects and
this.
Output
Ready to run.
Quick check
What does the first user.name read?
const user = { name: "Ada" };
console.log(user.name === "Ada"
Authoring rules
Practice
Reinforce this lesson with the Scope, Environments and Closures code-completion exercise and quiz.
Specification references
The normative model comes from the ECMAScript 2027 Language Specification — Environment Records and execution contexts. Environment Records model identifier bindings and their outer-environment links; execution contexts track the environment used to resolve identifiers. These are specification mechanisms, not objects that JavaScript code can directly inspect.
Next step
Now that scope and closures are in place, the curriculum can move into the broader function and object model: function objects,
this, call/apply behavior, constructors, and the prototype-based object model.
Thanks for your feedback.
Mark this learning resource complete to track your learning progress.