Inside JavaScript’s Mind: Lexical Environments and Scope Chains


Have you ever thought how JavaScript knows which variable you're referring to, even when it's not defined in your current function? How does JS decide where to look next when something is missing?
The answer lies deep within JavaScript’s core architecture — Lexical Environments and the Scope Chain.
🔍 What is a Lexical Environment?
A Lexical Environment is JavaScript's internal data structure that tracks variable and function declarations along with their values.
Think of it as a filing cabinet where each drawer (scope) contains labeled folders (variables) and knows which cabinet it's inside of.
Every time:
A function is invoked, or
A new block scope is entered (
let
,const
),
a fresh Lexical Environment is created.
Every lexical environment has two key components:
Environment Record - stores variable/function bindings
Outer Environment Reference - points to the parent scope
function outer() {
let a = 10;
function inner() {
console.log(a); // looks in inner's LE → not found → outer's LE → found!
}
inner();
}
Environment Record Types
JavaScript uses different record types for different contexts:
Function Environment Record - for function scopes
Declarative Environment Record - for block scopes (
let
,const
)Global Environment Record - for global scope
Object Environment Record - for
with
statements
🌐 Types of Scopes in JavaScript
Scopes define where your variables live:
Global Scope
- Declared outside any function/block.
Function Scope
- Created inside a function.
Block Scope
let
andconst
make block-level scopes (within{}
).
🧵 The Scope Chain: JS’s Variable Lookup System
When JavaScript can’t find a variable in the current LE, it “climbs the scope chain” by following outer references until it either:
Finds the variable.
Reaches the global environment and throws a
ReferenceError
.
let myVar = "global";
function outer() {
let myVar = "outer";
function inner() {
console.log(myVar); // Prints "outer"
}
inner();
}
outer();
Lexical vs Dynamic Scoping
JavaScript uses lexical scoping, this means that the scope of variables is determined by where the variables are declared in the code and not where functions are called.
let outerVar = "global";
function printVar() {
console.log(outerVar);
}
function testScoping() {
let outerVar = "local";
printVar(); // Prints "global" - lexically bound to global outerVar
}
testScoping();
This works different from dynamic scoping, where printVar()
would print "local" based on the calling context.
Variable Hoisting and Lexical Environments
Hoisting occurs during the lexical environment creation phase:
console.log(hoistedVar); // undefined (not ReferenceError)
console.log(hoistedFunc); // [Function: hoistedFunc]
var hoistedVar = "I'm hoisted";
function hoistedFunc() {
return "I'm also hoisted";
}
// let and const behave differently
console.log(notHoisted); // ReferenceError: Cannot access before initialization
let notHoisted = "I'm not hoisted";
Why this happens:
var
declarations are hoisted and initialized withundefined
Function declarations are fully hoisted
let
/const
are hoisted but remain in "temporal dead zone"
🔐 Closures – The Power of Retention
Closures are functions that retain access to their outer scope, even after that outer function has returned.
function counter() {
let count = 0;
return function () {
count++;
console.log(count);
};
}
const inc = counter();
inc(); // 1
inc(); // 2
The inner function remembers count
due to the scope chain.
⚠️ Pitfalls & Misunderstandings
Shadowing: Inner variable masks outer variable.
Temporal Dead Zone: Accessing
let
/const
before declaration.Confusing call stack with scope chain: Remember, where it’s defined, not where it’s called.
⛳️ Conclusion
Understanding Lexical Environments and the Scope Chain gives you superpowers when debugging or writing advanced JavaScript.
Next time your code throws an undefined variable error, you’ll know exactly where JavaScript looked — and why it couldn’t find what it needed.
✍️ Missed My Last Blog?
Read it here.
🔍 Uncover how JavaScript reorders your code behind the scenes through hoisting. Learn what actually gets hoisted during the compilation phase, how
var
,let
, andconst
behave differently, and how understanding hoisting helps you avoid subtle bugs and write cleaner code.
🤝 Connect with Me
If you enjoyed this blog and want to stay updated with more JavaScript insights, developer tips, and tech deep-dives — feel free to connect with me across platforms:
🔗 LinkedIn: Connect with me on LinkedIn
📝 Medium: Follow me on Medium
🌐 Hashnode: Check out my Hashnode blog
I appreciate your support and look forward to connecting with fellow devs, learners, and curious minds like you! 🚀
Subscribe to my newsletter
Read articles from K Manoj directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

K Manoj
K Manoj
Backend Web Developer | Security Enthusiast |