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:

  1. 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.
  2. 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 null or 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()); // 2

When 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()); // 11

Both 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.