# JavaScript Lexical Scope: Compile-Time vs Runtime Resolution

> Understand JavaScript lexical scope resolution at compile time versus runtime. Learn how the engine builds a scope tree and traverses it efficiently.

- Repository: [Kyle Simpson/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
- Tags: deep-dive
- Published: 2026-02-24

---

**JavaScript’s lexical scope is determined during the compile-time parsing phase when the engine builds a static scope tree, and at runtime the engine simply traverses this pre-built chain to resolve variable references.**

The distinction between compile-time and runtime behavior in JavaScript lexical scoping is fundamental to understanding hoisting, closures, and the temporal dead zone. According to the *You-Dont-Know-JS* repository by getify, the JavaScript engine performs a parsing phase that establishes all lexical environments before any code executes.

## What Is Lexical Scope in JavaScript?

Lexical scope (also called static scope) means that a variable's accessibility is determined by its physical position within the source code nesting. As documented in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md) of the *You-Dont-Know-JS* series, the scope chain is established by where functions and blocks are written, not where they are called.

## Compile-Time: Building the Scope Chain

During the compile-time (parsing) phase, the JavaScript engine walks the source tree and creates **environment records** for each function, block, and catch construct. This process fixes the location of every identifier in the source code.

### Environment Records and Identifier Binding

According to [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md), the parser records where each `var`, `let`, `const`, `function`, and `class` declaration appears. This binding process determines which lexical environment owns each variable before any code runs.

### Hoisting and Declaration Processing

The compile-time phase handles **hoisting** by moving `var` and function declarations to the top of their containing scope. As shown in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md), this means the scope tree contains these bindings immediately, though `var` variables remain uninitialized until their assignment executes.

## Runtime: Resolving Variables Through the Static Chain

At runtime, the engine does not re-evaluate where variables were declared. Instead, it uses the pre-built lexical chain to resolve references.

### Lexical Environment Lookup

When the engine encounters a variable reference, it searches the current **Lexical Environment** and walks outward through parent environments. As detailed in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md), this lookup follows the static scope chain established during parsing, not the dynamic call stack.

### Closures and Environment Capture

Closures demonstrate the compile-time vs runtime distinction clearly. According to [`scope-closures/ch4.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch4.md), when a function is defined, it captures a reference to its surrounding **environment record** (the storage for variables), not just the values. When the inner function executes later, it traverses this captured lexical chain to access the latest values:

```javascript
function outer() {
  let count = 0;               // ← declared in outer’s lexical environment
  return function inner() {    // ← inner closes over count
    return ++count;            // ← runtime lookup walks to outer’s env
  };
}
const inc = outer();           // outer finishes, but its environment stays alive
console.log(inc()); // 1
console.log(inc()); // 2

```

## Key Differences: Compile-Time vs Runtime

Understanding when specific scope behaviors occur helps debug hoisting issues and optimize performance:

- **Compile-time** determines *where* variables live. The parser builds the scope tree, processes hoisting for `var` and functions, and establishes the lexical hierarchy as documented in [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md) and [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md).
- **Runtime** determines *what* values variables hold. The engine traverses the pre-built lexical chain to fetch values, creates closures by capturing environment references, and enforces the **temporal dead zone** (TDZ) for `let`/`const` as described in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md).

## Practical Examples from You-Dont-Know-JS

The *You-Dont-Know-JS* repository provides concrete demonstrations of these concepts across several chapters.

### Var Hoisting vs TDZ

As shown in [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md), `var` hoisting occurs during compilation, while the TDZ is a runtime check:

```javascript
// Compile-time: var a is hoisted to function scope
console.log(a); // undefined – declaration exists, initialization pending
var a = 10;

// Compile-time: let b binding created in block scope
// Runtime: TDZ prevents access before initialization line
console.log(b); // ReferenceError – TDZ
let b = 20;

```

### Variable Shadowing

[`scope-closures/ch5.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch5.md) demonstrates how compile-time scope nesting creates shadowing:

```javascript
function foo() {
  let a = 1;
  function bar() {
    let a = 2; // Compile-time: new binding shadows outer 'a'
    console.log(a); // Runtime: finds inner 'a' (2)
  }
  bar();
  console.log(a); // Runtime: finds outer 'a' (1)
}

```

### Named Function Expressions

According to [`scope-closures/ch6.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch6.md), named function expressions create a binding at compile-time that is only available inside the function:

```javascript
const myFunc = function namedFunc() {
  // Compile-time: 'namedFunc' is bound in this function's scope only
  console.log(typeof namedFunc); // "function"
};
console.log(typeof namedFunc); // "undefined" – not available in outer scope

```

## Summary

- **Lexical scope is static**: JavaScript determines variable accessibility during the compile-time parsing phase by analyzing source code nesting, not runtime call patterns.
- **Compile-time builds the tree**: The engine creates environment records, processes hoisting for `var` and functions, and establishes the lexical hierarchy as documented in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md) through [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md).
- **Runtime follows the chain**: Execution resolves variables by traversing the pre-built lexical environment chain, captures closures by referencing environment records, and enforces the temporal dead zone for `let`/`const`.
- **Closures rely on this distinction**: Inner functions capture references to outer environment records at definition time (compile-time), allowing access to those variables long after the outer function completes.

## Frequently Asked Questions

### What is the difference between lexical scope and dynamic scope?

**Lexical scope** (also called static scope) means that a variable's accessibility is determined by its physical location in the source code at compile-time. **Dynamic scope** would mean that variable access depends on the call stack at runtime. JavaScript uses exclusively lexical scoping, as established in [`scope-closures/ch1.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch1.md), meaning the scope chain is fixed during parsing and does not change based on how functions are invoked.

### Does JavaScript re-evaluate scope chains at runtime?

No. Once the JavaScript engine completes the compile-time parsing phase, the lexical scope chain is immutable. At runtime, the engine only traverses this pre-built chain to resolve variable references. As demonstrated in [`scope-closures/ch4.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch4.md), when a closure captures an environment, it captures a reference to the compile-time environment record, not a snapshot of values, allowing it to access updated variable states through the static chain.

### How does hoisting work if scope is determined at compile-time?

Hoisting is a compile-time behavior. During the parsing phase, the JavaScript engine scans the source code and moves `var` declarations and function declarations to the top of their containing scope, creating bindings in the environment record. As explained in [`scope-closures/ch2.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch2.md) and [`scope-closures/ch3.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch3.md), this means the identifier exists throughout the scope at runtime, though `var` variables remain uninitialized (`undefined`) until their assignment line executes, while `let` and `const` remain in the temporal dead zone until initialization.

### Why do closures work after the outer function returns?

Closures work because JavaScript's lexical scoping captures environment records, not just values. When an inner function is defined, it captures a reference to the outer function's lexical environment (created at compile-time). Even after the outer function finishes executing and its execution context pops off the call stack, the environment record remains alive in memory because the inner function maintains a reference to it. As shown in [`scope-closures/ch4.md`](https://github.com/getify/You-Dont-Know-JS/blob/main/scope-closures/ch4.md), subsequent calls to the inner function traverse this captured lexical chain to access and modify the shared variables.