JavaScript Lexical Scope: A Complete Tutorial
Lexical scope means a function's visibility is determined by where it is written, not where it is called. Learn how scope chains work and why nesting matters.
Lexical scope is the rule that a function's access to variables is determined by where the function is written in the source code, not where it is called. "Lexical" means "related to the text" -- the physical placement of your code dictates what variables are visible.
const outer = "I am in the outer scope";
function inner() {
console.log(outer); // Works: outer is in a parent lexical scope
}
inner(); // "I am in the outer scope"The inner function can see outer because it is written inside the same scope where outer is declared. This has nothing to do with where inner is called. If you moved the call to a different file or passed inner as a callback, it would still see outer.
Lexical vs Dynamic Scope
JavaScript uses lexical scope (also called static scope). Some languages use dynamic scope. The difference:
| Lexical Scope | Dynamic Scope | |
|---|---|---|
| Visibility depends on | Where the function is written | Where the function is called |
| Determined at | Definition time | Call time |
| Used by | JavaScript, most modern languages | Some shells and older languages |
JavaScript is purely lexical. This matters when functions are passed around:
const name = "Global";
function greet() {
console.log(`Hello, ${name}`);
}
function runInDifferentContext(fn) {
const name = "Local";
fn(); // Which name does it use?
}
runInDifferentContext(greet); // "Hello, Global" -- lexical, not dynamicIf JavaScript used dynamic scope, greet would print "Hello, Local" because it was called inside runInDifferentContext where name is "Local". But JavaScript uses lexical scope, so greet prints "Hello, Global", it sees the name from where it was written, not where it was called.
The Scope Chain
When JavaScript encounters a variable name, it searches outward through nested scopes until it finds a match:
Each nested function adds a link to the chain. The search starts at the innermost scope and moves outward. The first match wins.
const value = "global";
function outer() {
const value = "outer";
function inner() {
const value = "inner";
console.log(value); // "inner" -- found in local scope first
}
inner();
}Calling outer triggers inner, and the lookup for value stops as soon as it finds a match, which happens immediately since inner declares its own value:
outer();JavaScript finds value in inner's local scope and stops there. It never reaches outer or the global scope. This is called variable shadowing: the inner declaration hides the outer one.
Scope Chain in Practice
A more realistic example shows the chain spanning three genuinely different scopes at once, rather than three declarations of the same name:
const appName = "MyApp"; // Global
function createUser(name) {
const id = crypto.randomUUID(); // Local to createUser
function formatUser() {
const prefix = "User:"; // Local to formatUser
// Can access: prefix (local), id (parent), name (parent param), appName (global)
return `${prefix} ${name} (${id}) - ${appName}`;
}
return formatUser();
}Calling createUser builds and returns a formatted string, pulling one piece of data from each level of the chain at once:
console.log(createUser("Alex"));
// "User: Alex (some-uuid) - MyApp"The formatUser function accesses variables from three different scopes: its own (prefix), its parent createUser (id, name), and global (appName). This is the scope chain in action.
Scope Is Determined at Definition, Not Execution
This is the key insight for understanding closures later:
function createPrinter(value) {
return function() {
console.log(value);
};
}
const printA = createPrinter("A");
const printB = createPrinter("B");
printA(); // "A"
printB(); // "B"Each returned function remembers the scope it was created in. printA's lexical scope contains a value of "A". printB's contains a value of "B". Where you call them does not change what they can see.
This is the foundation of JavaScript Closures Deep Dive: Complete Guide. For the basics of scope, see JavaScript Function Scope Local vs Global Scope.
Variable Shadowing
When an inner scope declares a variable with the same name as an outer scope, the inner one wins:
const count = 10;
function process(count) {
// The parameter 'count' shadows the global 'count'
console.log(count); // Uses the parameter, not the global
}
process(5); // 5
console.log(count); // 10 -- global is unchangedShadowing is not a bug. It is the intended behavior of lexical scope. The closest declaration wins. Just be aware of it -- if a variable seems to have the wrong value, check whether it is being shadowed by a closer declaration.
The Global Scope as the Final Link
The global scope is always the last link in every scope chain. In browsers, the global scope is the window object. In Node.js, it is the global object. In ES modules, the top-level scope is module scope, not global.
// In a browser script (not a module)
window.myGlobal = "accessible anywhere";
console.log(myGlobal); // "accessible anywhere"ES modules are safer because top-level variables are not truly global. They are scoped to the module unless exported.
Rune AI
Key Insights
- Lexical scope means scope is determined by where functions and blocks are written in the code.
- The scope chain is the hierarchy of scopes JavaScript searches to resolve a variable name.
- Inner functions can access variables from all enclosing scopes. Outer functions cannot see in.
- Variable shadowing occurs when an inner scope declares a variable with the same name as an outer one.
- Lexical scope is what makes closures possible in JavaScript.
Frequently Asked Questions
What is the difference between lexical scope and dynamic scope?
Does lexical scope mean I can access any variable from an outer function?
Is block scope the same as lexical scope?
Conclusion
Lexical scope is the rule that a function's visibility is locked in at the time it is written, not when it is called. JavaScript walks the scope chain outward -- from local to parent to global -- looking for variables. Understanding this one rule explains why closures work, why nested functions can see their parents, and why variable shadowing behaves the way it does.
More in this topic
JavaScript While Loop Explained: A Complete Guide
A while loop repeats code as long as a condition stays true. Learn the syntax, see practical examples, and understand when a while loop is the right choice over a for loop.
JavaScript Loops Tutorial: for, while, do while
Loops let JavaScript repeat code without writing it twice. Learn the three core loop types, what each one does, and when to choose one over another.
How to Loop Through Arrays Using JS for Loops Guide
Looping through arrays is the most practical use of JavaScript for loops. Learn how to access each element by index, avoid off-by-one errors, and choose between for, for...of, and forEach.