Inside JavaScript’s Mind: Lexical Environments and Scope Chains

K ManojK Manoj
4 min read

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:

  1. Environment Record - stores variable/function bindings

  2. 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:

  1. Global Scope

    • Declared outside any function/block.
  2. Function Scope

    • Created inside a function.
  3. Block Scope

    • let and const 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 with undefined

  • 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, and const 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! 🚀

10
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 |