Lexical scope
Lexical scope is the scope defined by where the code sits at the moment it is written, not at the moment it runs. Which function has access to which variables is decided at declaration time, when the engine reads the source, not when you call the function.
Theory
TL;DR
- Lexical scope is determined by where the code is placed in the program text.
- Access to variables is decided at declaration and parse time, not at call time.
- An inner function sees the variables of every function it is nested in, plus the globals.
- Name lookup walks up the scope chain:
inner->outer->global. - Languages with dynamic scope (Bash, old Lisp) do the opposite: the caller decides.
- Closures are the practical consequence of lexical scope.
Quick example
let a = 10;
function outer() {
let b = 20;
function inner() {
let c = 30;
console.log(a, b, c); // 10 20 30
}
inner();
}
outer();Here inner is declared inside outer, so it has access to the variables of outer and to the globals. JavaScript memorised that structure at declaration, not at the call.
The scope chain:
inner -> outer -> globalWhy it is called "lexical"
The word "lexical" means "relating to the text of the program". The engine "remembers" where functions and blocks sit while parsing, when it reads the source file, and not when you call the functions.
The practical consequence: you can work out any function's scope just by looking at the text of the file, without running the program. That is exactly why linters and IDEs can highlight unreachable or shadowed variables before anything executes.
Lexical versus dynamic scope
Some languages (Bash or old Lisp, for example) use dynamic scope: it is determined by whoever called the function. In JavaScript the scope is lexical: it is determined by where the function is written.
let x = "global";
function foo() {
console.log(x);
}
function bar() {
let x = "local";
foo(); // who called foo? bar, but...
}
bar();Result:
globalBecause foo is declared in the global scope, JavaScript looks for x there, not at the place it was called from (bar).
This is the essence of lexical scope: "look where the function is written, not where it is called from".
Closures, lexical scope in practice
function makeCounter() {
let count = 0;
return function() {
count++;
console.log(count);
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2Here the inner function memorised the lexical environment of makeCounter and still "sees" count even after the outer function has finished.
This works only because the scope is lexical and not dynamic: the reference to the outer environment is baked into the function when it is created, so it lives as long as the function itself.
Visually
[ Global Scope ]
a = 10
|
[ outer() Scope ]
b = 20
|
[ inner() Scope ]
c = 30The function inner always "knows" where to look for a and b, because that was fixed at declaration time.
Summary
| Concept | Description |
|---|---|
| Lexical scope | The scope in which a function "sees" variables; determined by its position in the source code |
| Determined when | While the code is written and parsed, not while it runs |
| Why it exists | So the engine knows which variables are available to each function |
| Related to | Lexical Environment, Scope Chain, Closures |
Common mistakes
- Assuming scope depends on the call site. It does not, only on the declaration site in the program text.
- Expecting
foo()in the example above to see the localxfrombar. That is dynamic scope behaviour, which JavaScript does not have. - Confusing lexical scope with
this. In ordinary functionsthisis in fact resolved dynamically, at call time; only arrow functions take it lexically. - Thinking that an outer function's variables are guaranteed to disappear once it returns. If a closure holds a reference to them, they stay alive.
- Confusing lexical scope with hoisting. Hoisting answers "when does a variable become usable", lexical scope answers "where do we look for it at all".
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.