JavaScript Scope
Scope determines where variables are accessible in your code. JavaScript has three types of scope: global, function, and block. Understanding scope is crucial for writing organized, bug-free code and avoiding naming conflicts.
Scope Types
Global scope contains variables declared outside any function or block. Global variables are accessible everywhere in your code—from any function, block, or module. While convenient, excessive global variables can cause naming conflicts and make code hard to maintain. Minimize global scope usage.
Function scope contains variables declared inside a function. These variables are only accessible within that function and its nested functions—they don't exist outside. Each function creates its own scope bubble. Function scope prevents variables from leaking into the global scope and enables encapsulation.
Block scope, introduced with let and const in ES6, contains variables accessible only within a block {}. Blocks are created by if statements, loops, or standalone braces. Block-scoped variables are confined to their block, providing fine-grained control over variable lifetime and preventing accidental reuse.
Variables declared with var have function scope (or global if outside functions), not block scope. This means var variables can "leak" out of blocks like if statements or loops. This unintuitive behavior is why let and const, which have block scope, are preferred in modern JavaScript.
Variables declared with let and const have block scope, meaning they only exist within the block where they're declared. This predictable scoping makes code easier to understand and prevents bugs from variable pollution. Modern JavaScript strongly favors let and const over var for their better scoping behavior.
// Global scope
const globalVar = "I'm global";
function myFunction() {
console.log(globalVar); // Accessible
}
// Function scope
function test() {
var functionVar = "I'm local";
console.log(functionVar); // Works
}
// console.log(functionVar); // Error!
// Block scope
if (true) {
let blockVar = "I'm block scoped";
console.log(blockVar); // Works
}
// console.log(blockVar); // Error!
// var ignores block scope
if (true) {
var varVar = "I'm function scoped";
}
console.log(varVar); // Works!
Lexical Scope and Closures
Inner (nested) functions have access to variables from outer functions. An inner function can read and modify variables from any containing function, creating a scope chain. This access flows outward—inner functions can see outer variables, but outer functions cannot see inner variables. This principle drives JavaScript's scoping system.
This behavior is called lexical scoping (or static scoping). "Lexical" means the scope is determined by where functions are written in the code, not where they're called from. A function's scope is fixed at the time it's defined based on its physical location in the source code.
Closures are functions that remember and can access their outer (lexical) scope even after the outer function has finished executing. When a function returns an inner function, that inner function "closes over" the outer function's variables, keeping them alive. This enables powerful patterns like private variables and function factories.
Closures are invaluable for data privacy and encapsulation. By returning a function that accesses private variables, you can create truly private data that's only accessible through the returned function. The private variables are hidden in the closure, inaccessible from outside. This implements the module pattern and data hiding.
Every function creates its own scope when called. This scope contains the function's parameters and local variables. Functions can be nested infinitely deep, with each creating its own scope layer. This nested scope structure forms a scope chain that JavaScript traverses when resolving variable references.
// Lexical scope
function outer() {
const outerVar = "I'm outer";
function inner() {
console.log(outerVar); // Has access
}
inner();
}
// Closure
function counter() {
let count = 0;
return function() {
count++;
return count;
};
}
const increment = counter();
console.log(increment()); // 1
console.log(increment()); // 2
console.log(increment()); // 3