Why Functions Remember Where They Come From: Understanding Lexical Scope in JavaScript
Lexical scope in JavaScript means functions access variables based on where they are defined in code, creating a predictable scope chain and enabling powerful features like closures and consistent behavior in arrow functions.
Thereâs a moment in JavaScript where something feels almost⌠mysterious.
A function accesses a variable you never passed into it.
It just âknowsâ where to get it from.
And naturally, the question comes up:
Where did that value come from?
The answer lies in something fundamental.
Lexical scope.
Scope Isnât About Execution â Itâs About Structure
At first, itâs tempting to think that scope depends on how code runs.
But JavaScript doesnât think that way.
Instead, it asks:
Where was this function written?
Lexical scope means:
A function can access variables based on where it is defined, not where it is called.
That one idea explains a lot of JavaScript behavior.
Following the Chain
When a function tries to access a variable, it doesnât search randomly.
It follows a path.
First, it looks inside itself.
If it doesnât find the variable, it looks at the outer function.
Then the outer of that.
And eventually, the global scope.
const a = 10
function outer() {
const b = 20
function inner() {
console.log(a, b)
}
inner()
}Here, inner can access both a and b.
Not because they were passed in.
But because of where inner was defined.
The Important Misconception
A common mistake is thinking scope depends on where a function is called.
Letâs challenge that idea.
const a = 10
function printA() {
console.log(a)
}
function run() {
const a = 20
printA()
}
run()If scope were based on execution, this would print 20.
But it prints 10.
Because printA was defined in the global scope.
It doesnât care where itâs called.
It only cares where it was created.
Scope Only Goes Upward
Another important rule is direction.
A function can access variables from outer scopes.
But not from inner ones.
function outer() {
const a = 10
function inner() {
const b = 20
}
console.log(b) // error
}The outer function cannot âseeâ into the inner function.
Scope flows upward, not downward.
Why This Matters: Closures
Lexical scope becomes much more powerful when functions outlive their original context.
function createCounter() {
let count = 0
return function () {
count++
console.log(count)
}
}
const counter = createCounter()
counter() // 1
counter() // 2Even after createCounter finishes executing, the inner function still has access to count.
It remembers where it was created.
This is called a closure.
And it only works because of lexical scope.
Arrow Functions and Lexical Behavior
Arrow functions take lexical scope even further.
They donât just inherit variables.
They also inherit things like this.
function Timer() {
this.seconds = 0
setInterval(() => {
this.seconds++
console.log(this.seconds)
}, 1000)
}The arrow function doesnât define its own this.
It uses the one from its surrounding scope.
This makes behavior more predictable, especially in callbacks.
A Different Way to Think About Functions
Instead of seeing functions as isolated blocks of codeâŚ
itâs more accurate to see them as:
functions + the environment where they were created
That environment stays with them.
No matter where they are executed later.
The Bigger Insight
Lexical scope is not just a rule.
Itâs a design choice.
JavaScript decided that code should behave based on structureâŚ
not on runtime context.
That makes reasoning about code more predictable.
Even if it feels unintuitive at first.
A Final Thought
When a function runs, it doesnât look around randomly for variables.
It follows a path that was defined the moment it was written.
It carries its surroundings with it.
And once you understand thatâŚ
JavaScript stops feeling magical.
And starts feeling consistent.