Lexical Environment & Scope Chain
V8 parse time pe decide karta hai — runtime pe sirf dhundta hai.
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.
// 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();
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
• 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
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!
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
}
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; }
}
• 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
Want to track your progress?
Log in to save your place and pick up where you left off.
Progress track karna chahte ho?
Login karo apni progress save karne ke liye aur jahan chhoda tha wahan se shuru karo.
Login