Chapter 2.4☕ 20 min read

Lexical Environment & Scope Chain

V8 parse time pe decide karta hai — runtime pe sirf dhundta hai.

01Lexical Environment — Ek Record Aur Ek Link

A Lexical Environment is V8's internal structure for managing scope. It consists of two parts: an Environment Record that stores variable bindings for the current scope, and an Outer Reference — a pointer to the parent Lexical Environment.

"Lexical" means: determined by WHERE the code is written (position in source), not WHERE it's called. Every function, block (if/for/while), and the global scope gets its own Lexical Environment. let and const bindings live in the LE, while var lives in the separate Variable Environment.
// Each block creates a new Lexical Environment

const globalVar = 'global'; // Global Lexical Environment

function outer() {
  // outer's Lexical Environment
  // Environment Record: { outerVar: 'outer' }
  // Outer Reference: → Global Lexical Environment
  const outerVar = 'outer';

  function inner() {
    // inner's Lexical Environment
    // Environment Record: { innerVar: 'inner' }
    // Outer Reference: → outer's Lexical Environment
    const innerVar = 'inner';

    // Can access all outer scopes via the chain:
    console.log(innerVar);  // 'inner' — own scope
    console.log(outerVar);  // 'outer' — outer's scope
    console.log(globalVar); // 'global' — global scope
  }

  inner();
}
outer();
In V8's implementation: the Lexical Environment is called a "Context" object. Each context has a pointer to its parent context (the outer reference). Closures work because inner functions hold a reference to their outer context object — even after the outer function returns!
02Scope Chain — Upar Upar Dhundna

The scope chain is a linked list of Lexical Environments connected via outer references. When you access a variable, V8 starts in the current Environment Record → if not found, follows the outer reference → repeats until found or the chain ends at global.

Lookup rules:

// Scope chain traversal in action

const x = 'global';

function outer() {
  const x = 'outer'; // shadows global x

  function inner() {
    const x = 'inner'; // shadows outer x
    console.log(x);    // 'inner' — found in own record
  }

  function noShadow() {
    console.log(x);    // 'outer' — not in own record, found in outer's
  }

  inner();    // 'inner'
  noShadow(); // 'outer'
}

outer();
console.log(x); // 'global' — outer scope unchanged

// Closure mutating outer variable:
function makeCounter() {
  let count = 0; // in makeCounter's Environment Record
  return function() {
    count++;      // writes to outer LE — this IS the closure
    return count;
  };
}
const counter = makeCounter();
counter(); // 1
counter(); // 2 — count persists in makeCounter's LE
📋 Shadowing is intentional but dangerous:
• Declaring the same name in inner scope shadows outer — inner record is found first
• Common bug: function parameter named same as outer variable — parameter wins!
• Shadowing is NOT an error, but you'll get wrong values with no warning
03Parse Time — Pehle Se Decide

Lexical scoping means scope is determined at PARSE TIME (when V8 reads your code), not runtime. The parser establishes the entire scope chain before any code executes.

This is why closures work predictably: V8 knows at parse time exactly which variables each function will need from outer scopes.

// Lexical scope — determined at PARSE TIME

const value = 'outer';

function getValue() {
  return value; // V8 knows at parse time: 'value' from global scope
}

function callFromDifferentContext() {
  const value = 'inner'; // Different value in this function
  return getValue();     // getValue still uses OUTER 'value'
}

callFromDifferentContext(); // 'outer' — NOT 'inner'!
// If JS used dynamic scope, it would return 'inner'
// But JS is LEXICAL — getValue's scope chain was fixed at parse time

// with statement — breaks lexical scope (forbidden in strict mode)
// with (someObject) { x = 1; } // Is 'x' a property or a global?
// V8 can't know until runtime — kills all optimization!
TurboFan optimization depends on this: Since the scope chain is fixed at parse time, V8 knows at compile time where each variable lives. It can inline the access, skip chain traversal, even keep variables in CPU registers. Dynamic scoping would make this impossible — which is why with is banned in strict mode.
04Closures — LE Zinda Rehta Hai

A closure is a function plus its captured Lexical Environment. When an outer function returns, its stack frame is destroyed — but if an inner function was returned, the captured LE stays alive on the heap.

function createMultiplier(factor) {
  // factor lives in createMultiplier's Lexical Environment
  return function(number) {
    return number * factor; // inner function captures outer LE
  };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

// createMultiplier's stack frame is gone — BUT
// double still holds reference to its captured LE (factor=2)
// triple still holds reference to its captured LE (factor=3)

double(5); // 10 — factor=2 still accessible!
triple(5); // 15 — factor=3 still accessible!

// Each closure keeps ONE separate copy of its captured LE:
// double's LE: { factor: 2 }
// triple's LE: { factor: 3 }
// These are separate objects in the heap

// Potential memory leak:
function processLargeData() {
  const bigArray = new Array(1000000).fill(0); // 8MB
  return function() {
    return bigArray.length; // captures entire bigArray!
  };
}
const fn = processLargeData(); // bigArray stays in memory

// Fix: extract only what's needed
function processSmarter() {
  const bigArray = new Array(1000000).fill(0);
  const len = bigArray.length; // capture only the length
  return function() { return len; }; // bigArray can be GC'd
}
Memory cost is real: Each closure keeps its captured LE alive on the heap. Closures over large objects cause memory leaks if the closure lives long. Extract only what you need before closing over it.
05Block Scope — let/const Ka Naya Kamra

ES6+ creates a new Lexical Environment for every {} block containing let/const. This has profound implications:

// Block scope — new LE per {}
{
  let blockScoped = 'only here';
  var functionScoped = 'leaks out';
}
// console.log(blockScoped);    // ReferenceError ✅
console.log(functionScoped);    // 'leaks out' — var escapes block

// for...let — new LE per iteration (the closure fix!)
const fns = [];
for (let i = 0; i < 3; i++) {
  // New LE created for each iteration with its OWN 'i'
  fns.push(() => i);
}
console.log(fns[0]()); // 0 — captured its own i=0
console.log(fns[1]()); // 1 — captured its own i=1
console.log(fns[2]()); // 2 — captured its own i=2

// switch — SHARED LE (surprising!)
switch (true) {
  case true:
    let value = 'first';
    break;
  case false:
    console.log(value); // 'first' accessible! Same LE!
    break;
}

// Fix: wrap case bodies in {}
switch (true) {
  case true: { let value = 'isolated'; break; }
  case false: { let value = 'also isolated'; break; }
}
📋 for...let is ES6's gift to closures:
• var: all iterations share ONE variable — closures see final value
• let: each iteration gets its own LE with its own i — closures capture independent values
• This is pure Lexical Environment mechanics, not magic!

Lo kar liya — Key Points:

  • ✅ Lexical Environment = Environment Record (bindings) + Outer Reference (parent scope) — created for every scope
  • ✅ Scope chain: V8 walks outer references until it finds the variable or hits global — then ReferenceError
  • ✅ Lexical scope is determined at PARSE TIME — not call time — this is why closure behavior is predictable
  • ✅ Closure = function + captured outer Lexical Environment — outer LE lives on heap as long as closure is alive
  • ✅ for...let creates a new Lexical Environment per iteration — each closure captures its own independent variable
  • ✅ switch statement shares ONE Lexical Environment — let in one case is accessible in other cases
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase