Lexical Scope & the Scope Chain
Introduction
Lexical scope means JavaScript decides which variables a function can access by where that function is written in the source code, not where it is called.
The scope chain is the ordered set of environments JavaScript searches when resolving a name: local scope first, then outer lexical scopes, and finally global scope.
Why This Matters
This is the exact mental model interviewers expect before closures. A function passed across your program does not adopt the caller's local variables. It keeps access to the variables from the place it was created.
Theory
Lexical means source-position based
A function's accessible outer variables are determined at creation time from the surrounding source code. Calling the function from a different place does not change that outer environment. This is also called static scope.
The scope chain algorithm
When JavaScript reads an identifier like userId, it searches:
- the current function or block environment,
- the next outer lexical environment,
- continuing outward one environment at a time,
- the global environment,
- and if still not found, a direct read throws
ReferenceError.
Shadowing
If an inner scope declares the same name as an outer scope, the inner binding shadows the outer one. Lookup stops at the first match. Shadowing is legal, but overusing it makes code harder to trace.
Lexical, not dynamic
| Question | JavaScript answer |
|---|---|
| Does a function use variables from where it is called? | No |
| Does a function use variables from where it is written/created? | Yes |
| Can an inner function see outer variables? | Yes |
| Can an outer function see inner variables? | No |
Visual Diagrams
global scope
label = 'global'
makePrinter() scope
label = 'outer'
printer() scope
read label
|
v
finds makePrinter label first
callFromElsewhere() scope
label = 'caller'
printer() still does NOT use this labelThe caller's local variables are not inserted into the callee's lexical scope chain.
Code Examples
Where a function is written matters
printer is called from callWithLocal, but it still reads label from makePrinter because that is where it was created.
Shadowing stops lookup early
The inner id hides the outer id. JavaScript does not keep searching after it finds a local match.
Output Prediction
Predict the output #1
1const label = 'global';2 3function makePrinter() {4 const label = 'outer';5 return function () {6 console.log(label);7 };8}9 10function callWithLocal(fn) {11 const label = 'caller';12 fn();13}14 15callWithLocal(makePrinter());Interview Questions
1What is lexical scope, and how is it different from dynamic scope?
Lexical scope means a function's accessible variables are determined by where the function is defined in the source code. Dynamic scope would mean variables are resolved based on the call stack or caller. JavaScript is lexically scoped: passing a function to another function does not make it see the caller's local variables.
Follow-ups
- How does this lead to closures?
- What is variable shadowing?
2Describe the scope chain lookup process.
Lookup starts in the current local environment. If the name is not found, JavaScript checks the next outer lexical environment, then the next, until it reaches the global environment. If no binding exists, reading the identifier throws ReferenceError.
Quiz
1. A function's outer variables are determined by…
2. If an inner scope and outer scope both declare `value`, which one is read inside the inner scope?
Summary
- Lexical scope is based on where functions and blocks are written, not where functions are called.
- The scope chain searches local scope, then outer lexical scopes, then global scope.
- Inner declarations can shadow outer declarations.
- Closures work because functions keep access to their lexical scope chain.