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.

6 min read

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.

javascriptjavascript
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 ScopeDynamic Scope
Visibility depends onWhere the function is writtenWhere the function is called
Determined atDefinition timeCall time
Used byJavaScript, most modern languagesSome shells and older languages

JavaScript is purely lexical. This matters when functions are passed around:

javascriptjavascript
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 dynamic

If 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:

JavaScript scope chain lookup

Each nested function adds a link to the chain. The search starts at the innermost scope and moves outward. The first match wins.

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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:

javascriptjavascript
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 unchanged

Shadowing 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 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.

javascriptjavascript
// 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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is the difference between lexical scope and dynamic scope?

Lexical (static) scope determines variable visibility based on where the function is written in the source code. Dynamic scope determines it based on where the function is called. JavaScript uses lexical scope exclusively.

Does lexical scope mean I can access any variable from an outer function?

Yes, as long as the variable is in a parent scope of where your function is defined. A function defined inside another function can access all variables from the outer function, the outer-outer function, and the global scope.

Is block scope the same as lexical scope?

Block scope is one type of lexical scope. Lexical scope is the broader principle that scope is determined by code structure. Block scope specifically refers to variables declared with let and const being confined to {} blocks.

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.