Skip to main content

Command Palette

Search for a command to run...

JS Fundamentals #21 Understanding Lexical Environment and Scope Chaining in JavaScript

Published
3 min read

JavaScript’s behavior around variable access, function execution, and closures can feel mysterious until you understand lexical environments and scope chaining. These concepts form the foundation of how JavaScript resolves identifiers (variables, functions) and manages closures. Let’s dive deep.


1. What is a Lexical Environment?

A lexical environment is a mechanism used by the JavaScript engine to keep track of variables, functions, and the scope in which they exist. You can think of it as a container that holds:

  • Environment Record: The actual mapping of variable names to values.

  • Reference to Outer Environment: A pointer to the environment in which this environment was created.

Every time a function or block executes, JavaScript creates a new lexical environment for it during the creation phase of the execution context.

Key Points:

  • Lexical environments are created during the creation phase of an execution context.

  • They are tied to where code is written, not where it is executed. This is why the term “lexical” is used—it comes from the lexical (written) scope of the code.

  • During the execution phase, the lexical environment is used for variable lookups and updates.


2. Structure of a Lexical Environment

A lexical environment typically looks like this conceptually:

LexicalEnvironment {
    EnvironmentRecord: {
        x: 10,
        y: 20
    },
    OuterEnvironment: <reference to outer lexical environment>
}
  • EnvironmentRecord stores all local bindings.

  • OuterEnvironment points to the outer scope (could be global).


3. How Lexical Environment Impacts Scope Chaining

Scope chaining is the process JavaScript uses to look up variables. When a variable is accessed, the engine searches the current lexical environment first. If it’s not found, it moves up the chain to outer environments until it finds it or reaches the global scope.

Example:

let a = 10;

function outer() {
    let b = 20;

    function inner() {
        let c = 30;
        console.log(a, b, c); // ?
    }

    inner();
}

outer();

Execution breakdown:

  1. Global Environment:

    • a is stored here.
  2. Outer Function Environment:

    • b is stored here.

    • OuterEnvironment → Global Environment

  3. Inner Function Environment:

    • c is stored here.

    • OuterEnvironment → Outer Function Environment

When console.log(a, b, c) runs:

  • JS engine first looks for a in Inner → not found

  • Looks in Outer → not found

  • Looks in Global → found (10)

  • Repeats for b → found in Outer (20)

  • c → found in Inner (30)

This is scope chaining in action, enabled by the lexical environment.


4. Closures and Lexical Environment

Closures are a direct consequence of lexical environments. When a function is created, it retains a reference to its lexical environment (the environment where it was defined).

function makeCounter() {
    let count = 0;

    return function () {
        count++;
        console.log(count);
    };
}

const counter = makeCounter();
counter(); // 1
counter(); // 2
  • The inner function retains access to count even after makeCounter has finished executing.

  • This is possible because the inner function’s [[Environment]] points to the outer lexical environment (makeCounter’s scope).


5. Key Takeaways

  1. Lexical environment: The hidden data structure JS uses to store variables and functions along with a reference to outer environments.

  2. Scope chaining: How JS resolves variable references by walking up the chain of lexical environments.

  3. Closures: Functions remember their lexical environment, allowing access to variables even after the outer function has returned.

  4. Lexical vs Dynamic Scope: JavaScript uses lexical scope, meaning where a function is defined matters more than where it is called.


Conclusion

Understanding lexical environments is critical for mastering JavaScript. It explains why closures work, why variables are accessible in certain scopes, and why scope chain lookups behave the way they do. Once you internalize this, debugging variable access, writing closures, and optimizing your code becomes much easier.