What lexical scope is in JavaScript
How does the engine know that one function can see another's variables, but not the other way around? In JavaScript it comes down to where the code sits, decided before anything runs. That's lexical scope.
When you write code, you intuitively know that a nested function sees the variables of the one that surrounds it - not the other way around:
function outer() {
const a = 1;
function inner() {
console.log(a); // 1, of course
}
inner();
}That intuition is stronger than it looks - it isn’t a habit picked up from coding conventions, it’s a rule baked into the language spec. It’s called lexical scope.
#Where the name lexical comes from
The term lexical points to the moment the engine analyzes your code, before running anything. That’s the same compilation phase where it also registers names for hoisting. At that point it finds every declaration and decides what is visible where. Scope follows from where a function or block sits in the source - not from who eventually calls it.
The opposite are languages with dynamic scope, where a variable’s visibility depends on the call context (early Lisp worked that way). In JS you can read the file and tell what a function has access to without running anything. Statically, lexically.
#What opens a new scope
JS opens a new scope in two places:
- A function - any function declaration or function expression.
- A block
{ ... }- when it containslet,const, orclass.varignores blocks and reaches up to the nearest function or to the global scope.
Nesting builds a chain (the scope chain): an inner scope sees the one that surrounds it, that one sees its own, all the way up to the global. The engine looks up names from the inside out - never the other way around.
#Shadowing
An inner scope can declare a variable with the same name as the surrounding one. It isn’t a conflict, it’s an override - shadowing. Inside you see the newly declared one; outside the surrounding scope’s value is still there:
let x = 1;
function f() {
let x = 2;
console.log(x); // 2
}
f();
console.log(x); // 1Two distinct slots in memory under the same name, each valid inside its own scope.
#Where the function name lands
A small detail about declarations vs expressions that’s easy to miss:
function declared() {} // the name "declared" goes to the parent scope
const x = function expr() {}; // the name "expr" stays INSIDE the functionA function declaration adds its name to the surrounding scope. A function expression - if it carries a name - only adds it to its own scope (so the function can call itself by name from inside). From the outside it is reachable only through the variable it was assigned to - here x, not expr.
#Closing thought
The question “where will the engine find a given name” is settled once, based on where the code physically sits in the file - before the function is ever called. The same fact comes back at hoisting (the engine registers names in each scope during the same compilation phase) and in closure (a function keeps the scope it was created in, not the one it’s called from).