JavaScript uses lexical scoping where variable access is determined by code structure at write time — the engine walks up the scope chain from inner to outer scopes until it finds the variable or reaches the global scope.
Variable lookup walks outward from the current scope through each enclosing scope until the global scope — this chain is fixed at write time.
var is function-scoped (ignores blocks), while let/const are block-scoped (confined to nearest {}). This is the most tested scope distinction.
A function's scope is determined by where it is defined, not where it is called — this is why closures work.
An inner scope variable with the same name hides the outer variable — the outer still exists but is inaccessible from the inner scope.
ES modules create their own scope — top-level declarations are not global and must be explicitly exported.
Scope determines where variables and functions are accessible in your code. JavaScript uses lexical (static) scoping, meaning scope is determined by the physical location of code when it's written, not when it's executed.
Global scope — Variables declared outside any function or block are globally accessible. In browsers, var declarations at the top level attach to window. Polluting global scope with too many variables causes naming collisions and hard-to-debug issues.
Function scope — Variables declared with var inside a function are accessible anywhere within that function, including nested blocks. Before ES6, function scope was the only way to create local variables.
Block scope — Variables declared with let and const are confined to the nearest enclosing {} block (if, for, while, or standalone blocks). This was introduced in ES6 and is the default scoping behavior in modern JavaScript.
Module scope — ES modules create their own scope. Top-level declarations in a module are not added to the global scope — they're only accessible within the module unless explicitly exported.
When JavaScript encounters a variable reference, it searches for the variable starting from the current scope and walking outward through each enclosing scope until it reaches the global scope. This chain of scopes is called the scope chain.
const global = 'G';
function outer() {
const outerVar = 'O';
function inner() {
const innerVar = 'I';
console.log(innerVar); // Found in inner scope
console.log(outerVar); // Found in outer scope (1 level up)
console.log(global); // Found in global scope (2 levels up)
}
inner();
}If the variable is not found in any scope, a ReferenceError is thrown (in strict mode) or an accidental global is created (in sloppy mode with assignment).
JavaScript uses lexical scoping — a function's scope is determined by where it is defined, not where it is called. This is why closures work: a function retains access to its defining scope's variables even when called from a different scope.
The only exception is this, which is determined dynamically at call time (unless arrow functions or .bind() are used).
When an inner scope declares a variable with the same name as an outer scope variable, the inner variable shadows the outer one. The outer variable still exists but is inaccessible from the inner scope.
const x = 'outer';
function example() {
const x = 'inner'; // Shadows outer x
console.log(x); // 'inner'
}
example();
console.log(x); // 'outer' — unchangedBefore ES6, the Immediately Invoked Function Expression was the primary way to create isolated scope and avoid polluting the global namespace:
(function() {
var private = 'hidden';
// This variable doesn't leak to global scope
})();With let, const, and ES modules, IIFEs are rarely needed in modern code, but they still appear in legacy codebases and some library patterns.
The most common scope-related interview question involves var in a for loop:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — because var is function-scoped, all callbacks share one i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 — because let creates a new binding per iterationKey Interview Distinction: Lexical Scope Enables Closures Scope and closures are deeply connected. A closure is a function that remembers its lexical scope. Understanding the scope chain is prerequisite to understanding why closures retain access to outer variables — the function carries a reference to its defining scope, not a copy of the variables.
Fun Fact
Brendan Eich was hired by Netscape in 1995 specifically to embed the Scheme programming language in the browser. Scheme's key innovation was lexical scoping — where a function's scope is determined by where it's written, not where it's called. When management forced Eich to make the language look like Java instead, he kept Scheme's lexical scoping as the foundation. This one decision is why closures work in JavaScript and why modern patterns like React hooks are possible.