How JavaScript Closures Capture Lexical Environments
In JavaScript, a closure is created whenever a function is defined, granting it access to variables from its surrounding scope even after that outer scope has finished executing. This mechanism works because every executing function creates a “lexical environment” object that stores local variables and holds a direct reference to its outer environment. When an inner function is retained, the JavaScript runtime maintains a reference to this environment chain in memory, preventing the outer variables from being garbage collected and allowing the inner function to access or modify them across its entire lifecycle.
The Structure of a Lexical Environment
To understand closures, you must first understand the Lexical Environment. Whenever JavaScript enters an execution context (such as invoking a function), the engine creates an internal Lexical Environment object composed of two key components:
- Environment Record: An actual record (like an internal key-value map) that stores local variable declarations, function declarations, and parameter values within that specific block or function.
- Outer Lexical Environment Reference: A pointer
linking the current environment to the lexical environment of the
enclosing scope. For top-level code, this pointer is
nullor points to the Global Environment.
How the Capture Mechanism Works
When an outer function runs, its execution context produces a new
Lexical Environment. If that outer function defines an inner function,
the JavaScript engine assigns an internal hidden property to that inner
function object, historically referred to in the ECMAScript
specification as [[Environment]].
This [[Environment]] internal slot permanently stores a
direct reference to the Lexical Environment in which the inner function
was instantiated.
function createCounter() {
let count = 0; // Stored in createCounter's Environment Record
return function() {
count++; // Accesses count via [[Environment]] reference
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2When createCounter() completes execution, standard
stack-based execution context memory is popped. However, the
counter variable now holds the returned inner function,
which contains the [[Environment]] reference pointing back
to the Lexical Environment where let count = 0 lives.
Memory Allocation and Garbage Collection
Normally, when a function completes execution, its local variables become unreachable and are marked for removal by the garbage collector.
With closures, the inner function maintains an active reference to the outer Lexical Environment. Because the inner function is accessible through the outer scope (e.g., assigned to a variable or passed as a callback), the garbage collector sees the entire lexical chain as reachable root references. As a result, the outer Lexical Environment is preserved on the heap rather than discarded.
Variables Are Captured by Reference, Not Value
A critical detail of lexical capture is that closures capture bindings (the variables themselves), not static values. If multiple inner functions are created within the same execution context, they share the exact same Lexical Environment:
function sharedScope() {
let value = 10;
return {
increment: () => ++value,
get: () => value
};
}
const instance = sharedScope();
instance.increment();
console.log(instance.get()); // 11Both increment and get have their
[[Environment]] pointing to the single Lexical Environment
created during the invocation of sharedScope(). When one
alters value, the other reads the updated state instantly
because they point to the identical Environment Record.