Lexical Scope: Understanding Variable Access
Lexical scope is the foundation of JavaScript's scoping system. It determines where variables can be accessed based on their position in the code, not where they are called. This is what makes closures possible.
What is Lexical Scope?
Lexical scope means that the accessibility of variables is determined by the position of the variables in the nested function scopes at write-time (or parse-time). A function can access variables that were available at the point where that function was defined.
Basic Lexical Scope
let globalVar = 'global';
function outer() {
let outerVar = 'outer';
function inner() {
let innerVar = 'inner';
console.log(innerVar); // 'inner' - local scope
console.log(outerVar); // 'outer' - outer scope (lexically)
console.log(globalVar); // 'global' - global scope
}
inner();
}
outer();Inner functions can access variables from their lexical (definition-time) environment, including outer function and global scopes.
Lexical Scope vs Dynamic Scope
JavaScript uses lexical scope, not dynamic scope. This means scope is determined by code structure, not by the call stack at runtime.
| Aspect | Lexical Scope | Dynamic Scope |
|---|---|---|
| Determined By | Code structure (write-time) | Call stack (runtime) |
| Uses | JavaScript, Python, Java | Some languages (Bash, older Lisp) |
| Predictability | Predictable - can read code | Unpredictable - depends on calls |
Why Lexical Scope Matters
let name = 'global';
function greet() {
console.log(name); // Will always use lexical 'name'
}
function wrapper() {
let name = 'wrapper scope';
greet(); // Calls greet from different context
}
greet(); // Output: 'global'
wrapper(); // Output: 'global' (NOT 'wrapper scope')
// With dynamic scope (hypothetical):
// wrapper() would output: 'wrapper scope' - wrong!Lexical scope ensures predictable variable resolution regardless of how functions are called.
Scope Levels in JavaScript
Global Scope
Global Scope - Accessible Everywhere
let globalVar = 'accessible everywhere';
function func1() {
console.log(globalVar); // Can access
}
function func2() {
console.log(globalVar); // Can access
}
func1(); // 'accessible everywhere'
func2(); // 'accessible everywhere'Variables in global scope are accessible from anywhere in the code.
Function Scope
Function Scope - Local to Function
function outer() {
let functionVar = 'local to outer';
function inner() {
console.log(functionVar); // Can access parent function scope
}
inner(); // 'local to outer'
}
// console.log(functionVar); // ReferenceError - not accessibleVariables in function scope are only accessible within that function and its nested functions.
Block Scope (let/const)
Block Scope with let and const
if (true) {
let blockVar = 'block scoped';
const blockConst = 'also block scoped';
var functionVar = 'function scoped'; // var ignores block scope!
console.log(blockVar); // Works
console.log(blockConst); // Works
}
// console.log(blockVar); // ReferenceError
// console.log(blockConst); // ReferenceError
console.log(functionVar); // Works - var is function scopedlet and const create block scope. var creates function scope (older behavior). Always use let/const.
The Scope Chain
When JavaScript looks for a variable, it searches through nested scopes from innermost to outermost until it finds the variable or reaches global scope.
Scope Chain Resolution
let level0 = 'global';
function level1() {
let level1Var = 'level 1';
function level2() {
let level2Var = 'level 2';
function level3() {
let level3Var = 'level 3';
// Searches: level3 → level2 → level1 → global
console.log(level3Var); // Found in local
console.log(level2Var); // Found in parent function
console.log(level1Var); // Found in grandparent function
console.log(level0); // Found in global
}
level3();
}
level2();
}
level1();Variable lookup follows the scope chain from innermost to outermost, stopping at the first match.
Variable Shadowing
When a variable in an inner scope has the same name as a variable in an outer scope, the inner variable "shadows" the outer one.
Variable Shadowing Example
let name = 'global';
function outer() {
let name = 'outer';
function inner() {
let name = 'inner';
console.log(name); // 'inner' - innermost scope wins
}
inner();
console.log(name); // 'outer'
}
outer();
console.log(name); // 'global'Inner scope variables shadow (hide) outer scope variables with the same name. The closest match is always used.
Common Lexical Scope Mistakes
❌ Confusing Dynamic and Lexical Scope
Wrong: Dynamic Scope Thinking
let result = 'global';
function func1() {
console.log(result); // Uses LEXICAL scope, not call time
}
function func2() {
let result = 'local';
func1(); // Output: 'global' (NOT 'local')
}
func2();Remember: scope is determined by code structure, not by where functions are called.
❌ Using var in Loops
Mistake: Block Scope with var
// WRONG - var creates function scope
for (var i = 0; i < 3; i++) {
// i is accessible everywhere in function
}
console.log(i); // 3 - still accessible!
// CORRECT - let creates block scope
for (let j = 0; j < 3; j++) {
// j is only accessible in loop
}
// console.log(j); // ReferenceErrorAlways use let/const for block scope. var creates function scope which often causes bugs.
❌ Creating Global Variables Accidentally
Mistake: Accidental Globals
function createGlobal() {
x = 'accidentally global'; // Creates global variable!
}
createGlobal();
console.log(x); // 'accidentally global'
console.log(window.x); // Also exists globally!
// CORRECT - Always declare variables
function correct() {
let x = 'properly scoped';
}
// Use strict mode to catch this
'use strict';
function strictMode() {
y = 'throws ReferenceError'; // Error in strict mode
}Always declare variables. Use 'use strict' mode to catch accidental globals.
Best Practices for Lexical Scope
- ✓Always use let/const, avoid var for predictable block scoping
- ✓Declare variables in the innermost scope where they're needed
- ✓Be aware of variable shadowing - avoid confusing variable names
- ✓Use 'use strict' mode to catch accidental globals
- ✓Keep scopes small and focused for better code readability
Related Topics
Frequently Asked Questions
Is lexical scope the same as static scope?
Yes, the terms are used interchangeably. Both refer to scope being determined at compile/parse time based on code structure.
Can lexical scope be changed at runtime?
No, lexical scope is fixed at parse time. You cannot modify which variables a function can access based on runtime conditions.
How does lexical scope relate to closures?
Closures exist exactly because of lexical scope. Lexical scope allows functions to remember and access the variables from where they were defined, even after those outer functions have completed.
What's the difference between lexical scope and execution context?
Lexical scope determines which variables a function CAN access. Execution context determines the environment when code is actually executed, including 'this' binding.