Lesson 7 · 11 min read

Scope Chain and Lexical Environment

Lesson 4 showed one function looking outward to Global. This lesson extends that into a full chain — as many levels deep as your code is nested — and gives it a proper name: lexical scope, decided entirely by where you physically wrote your code, never by which function called which.

On this page

  1. Concept
  2. Why this matters
  3. The chain of notice-boards — an analogy
  4. Lexical Environment — the formal definition
  5. Lexical (static) scope vs. dynamic scope
  6. Case 1: reading straight from global
  7. Case 2: nested functions, chain goes as deep as needed
  8. Case 3: shadowing — the nearest name wins
  9. Case 4: the one-way street
  10. See it live — DevTools scope panel
  11. Common misconceptions
  12. Interview questions

Concept

Lesson 4 gave one rule: a function can look outward to its parent scope, never sideways. A scope is the part of the code where a name can be seen and used. A nested function is a function written inside another function, and the outer one is its parent. This lesson stretches the rule across as many levels as your code has. A function nested three functions deep can still reach all the way up to global, checking one level at a time. That upward walk has a name: the scope chain. It is built from something you met briefly in Lesson 4: the Lexical Environment.

Why This Matters

  • It's the one rule behind every “why can this function see that variable?” question you will debug.
  • It explains exactly what happens when the same variable name exists at multiple levels — which one actually gets used, and why.
  • It's the foundation for closures (Lesson 10). A closure is a function that keeps access to the variables of the place where it was created, even after that place has finished running. You cannot understand closures until this lesson is clear.
  • Knowing JS is lexically scoped (not dynamically scoped) tells you that scope is decided when you write the code, not when it runs. Interviewers use this to check real understanding.

The Chain Of Notice-Boards — An Analogy

Walking up the chain of command, one office at a time

A trainee at the front desk needs a piece of information — say, today's discount rate. She first checks her own small notepad. Not there? She walks up to her direct supervisor's office and checks the board there. Still not there? She walks up again, to the branch manager's board. Still nothing? Finally, head office's master board — the one everyone in the whole building can see.

She always walks up, one office at a time, in order. She never walks sideways into a colleague's office at her own level, and she never walks back down into a junior trainee's desk below her. That upward-only, one-level-at-a-time walk is the entire scope chain.

Lexical Environment — The Formal Definition

A Lexical Environment is the place where the engine keeps the names and values of one scope. It is two things together: its own local memory, plus a reference to its parent's Lexical Environment (a reference is a link that points to something else). That is the whole mechanism. It repeats at every level, because the parent's Lexical Environment has the same two parts and points to its own parent, all the way up to Global. Global's reference points to null, a value that means “nothing here,” so the chain ends.

Scope vs. Lexical Environment, in one line

Scope is the rule: which part of the code can see which names. A Lexical Environment is the data the engine keeps to follow that rule: the memory of one scope plus the link to its parent.

When you look up a variable, JS checks the current Lexical Environment's local memory first. Not found? It follows that one reference upward and checks there. Still not found? It follows the next reference up. This upward walk, from one Lexical Environment to its parent, is what “the scope chain” means. The scope chain is the list of scopes JavaScript checks, in order, from the inside out.

Lexical (Static) Scope vs. Dynamic Scope

“Lexical” means based on where something is written in your code — not on which function called it. JavaScript decides scope this way. It helps to compare it with the other model:

Lexical (static) scope — what JS usesDynamic scope — a different model
Decided byWhere the function is physically written/nested in the source codeWhich function called it while the program runs
Fixed atThe moment you write the codeThe moment the code runs
Predictable fromReading the file aloneTracing the actual call sequence

A function's scope chain is decided when you write it inside another function in your source file. It has nothing to do with who calls it later. That is why you can find the chain just by reading the code, without running it. (JavaScript does not use dynamic scope.)

Case 1: Reading Straight From Global

case1.js
function frontDesk() {
  console.log(discountRate); // 10
}

var discountRate = 10;
frontDesk();

frontDesk's own local memory has no discountRate, so it follows its outer reference straight to Global. It finds the name there, with the value 10.

step 1 / 3
Running
frontDesk() is called

A new context with an empty local memory. Its Lexical Environment's outer reference points to where the function was written: Global.

case1.js
1function frontDesk() {
2 console.log(discountRate); // 10
3}
4
5var discountRate = 10;
6frontDesk();
(console)
Scope chain · inner → outer
frontDesk()
  • (no variables)
↓ outer reference
Global
  • frontDesk{ …code }
  • discountRate10
↓ outer reference
null
Step through the lookup: not in frontDesk's own memory, so one hop up the outer reference to Global. · interactive

Case 2: Nested Functions — The Chain Goes As Deep As Needed

case2.js
function frontDesk() {
  checkAvailability();

  function checkAvailability() {
    console.log(discountRate); // 10
  }
}

var discountRate = 10;
frontDesk();

checkAvailability is nested inside frontDesk. Its own memory has no discountRate — it checks its parent, frontDesk's memory. Not there either, so it keeps going up to Global, where it finds 10. This takes two hops up the chain.

checkAvailability()
(no discountRate here)
↓ outer reference · hop 1
frontDesk()
(no discountRate here either)
↓ outer reference · hop 2
Global
discountRate: 10 — found here
↓ null
two hops up, one level at a time, until the value is found
The lookup climbs one level per step — it never skips straight to the top. · scope chain
step 1 / 5
Running
frontDesk() is called

frontDesk's memory holds checkAvailability (hoisted, nested inside it). Its outer reference → Global.

case2.js
1function frontDesk() {
2 checkAvailability();
3
4 function checkAvailability() {
5 console.log(discountRate); // 10
6 }
7}
8
9var discountRate = 10;
10frontDesk();
(console)
Scope chain · inner → outer
frontDesk()
  • checkAvailability{ …code }
↓ outer reference
Global
  • frontDesk{ …code }
  • discountRate10
↓ outer reference
null
Step through the lookup: own memory, then frontDesk, then Global — one level per hop, never skipping straight to the top. · interactive

Case 3: Shadowing — The Nearest Name Wins

case3.js
function frontDesk() {
  checkAvailability();

  function checkAvailability() {
    var discountRate = 100;
    console.log(discountRate); // 100
  }
}

var discountRate = 10;
frontDesk();

This time checkAvailability has its own local discountRate, so the lookup stops at the first level. It does not continue up toward frontDesk or Global. The nearest match always wins. This is called shadowing: a variable in an inner scope has the same name as one in an outer scope, and the inner one “hides” the outer one for any code running inside the inner scope.

step 1 / 3
Running
checkAvailability() is called

Memory phase for the new context reserves its own discountRate → undefined. There are now two slots with that name on the chain.

case3.js
1function frontDesk() {
2 checkAvailability();
3
4 function checkAvailability() {
5 var discountRate = 100;
6 console.log(discountRate); // 100
7 }
8}
9
10var discountRate = 10;
11frontDesk();
(console)
Scope chain · inner → outer
checkAvailability()
  • discountRateundefined
↓ outer reference
frontDesk()
  • checkAvailability{ …code }
↓ outer reference
Global
  • frontDesk{ …code }
  • discountRate10
↓ outer reference
null
Step through the lookup: the local discountRate is found at the first level, so the chain is never climbed. Global's 10 is untouched. · interactive

Case 4: The One-Way Street

case4.js
function frontDesk() {
  var discountRate = 10;
  checkAvailability();

  function checkAvailability() {
    console.log(discountRate); // 10 — inner sees outer, as always
  }
}

frontDesk();
console.log(discountRate); // ReferenceError: discountRate is not defined

Global can never reach down into a function's local memory. The reference only points outward and upward, never the other way. This is the same one-way rule from Lesson 4, now stated in full: a function can access a global variable, but the global scope can never access a local variable declared inside a function.

step 1 / 6
Running
Inside frontDesk, checkAvailability() is called

frontDesk's memory holds discountRate: 10. checkAvailability's outer reference → frontDesk.

case4.js
1function frontDesk() {
2 var discountRate = 10;
3 checkAvailability();
4
5 function checkAvailability() {
6 console.log(discountRate); // 10
7 }
8}
9
10frontDesk();
11console.log(discountRate); // ReferenceError
(console)
Scope chain · inner → outer
checkAvailability()
  • (no variables)
↓ outer reference
frontDesk()
  • discountRate10
  • checkAvailability{ …code }
↓ outer reference
Global
  • frontDesk{ …code }
↓ outer reference
null
Step through both lookups: the inner one climbs up and succeeds; the global one has no way to climb down, and fails. · interactive

See It Live — DevTools Scope Panel

  1. Set a breakpoint

    On the console.log(discountRate) line inside checkAvailability in the Case 2 example.

  2. Open the Scope panel

    Sources tab, while paused at that breakpoint.

  3. Read the panel top to bottom

    The panel lists the scopes from the inside out. First is Local (checkAvailability's own scope, without discountRate), and last is Global, where discountRate: 10 shows up. DevTools shows a Closure (functionName) entry for an outer function only when the inner function uses one of that outer function's variables. In Case 2 it does not, so you will normally see only Local and Global. To see a Closure entry, add var floor = 3; inside frontDesk and print it from checkAvailability. The panel shows the scope chain in the order the engine walks it.

Common Misconceptions

“A function's scope depends on which function called it.” No — that would be dynamic scope, which JS doesn't use. A function's scope chain is fixed by where it is written and nested in the code, before it runs.

“If a local variable shadows a global one, the global value is lost forever.” It isn't lost. It is hidden only for code running inside that inner scope. The global variable is unchanged and available everywhere else.

“Global scope can peek into any function's variables if it really needs to.” It can't. The chain only runs outward and upward, never down into a function's private memory.

Interview Questions

What is a Lexical Environment?

The place where the engine keeps one scope's names and values: its own local memory, plus a reference (link) to its parent's Lexical Environment. This repeats all the way up to Global, which points to null (nothing).

What is the scope chain?

The list of scopes that JS checks, from the inside out, when a variable isn't found locally: the parent's Lexical Environment, then its parent, one level at a time, until the variable is found or Global is reached.

Is JavaScript lexically scoped or dynamically scoped?

Lexically (statically) scoped. A function's scope is fixed by where it is written and nested in the source code, not by which function calls it while the program runs.

What happens when a local variable has the same name as a global one?

Shadowing. The local variable wins for any code running inside that inner scope. The global one is unchanged, only hidden from that scope.

Can the global scope access a variable declared inside a function?

No — the scope chain only runs outward and upward. A function can reach a global variable, but global can never reach back down into a function's local variables.

If a variable isn't found anywhere in the entire scope chain, including Global, what happens?

A ReferenceError — the name was never defined anywhere reachable from where you're standing.

Published by Jainik · Last updated · How these lessons are made

Keep learning

Part of the study guide

Related series