JavaScript Closures Tutorial - Section 1: Fundamentals
Lexical scope (also called static scope) means that the scope of a variable is determined by its position in the source code. In other words, where you write your code determines what variables are accessible.
This is different from dynamic scope (used in some other languages), where scope is determined by the call stack at runtime. JavaScript uses lexical scope exclusively.
const globalVar = "global";
function outer() {
const outerVar = "outer";
function inner() {
const innerVar = "inner";
console.log(innerVar); // ✓ Can access
console.log(outerVar); // ✓ Can access
console.log(globalVar); // ✓ Can access
}
inner();
}
outer();
The inner function can access variables from its own scope, its parent's scope (outer), and the global scope. This is determined by where inner is written in the code, not where it's called.
When JavaScript looks for a variable, it follows the scope chain:
Let's see how lexical scope determines variable accessibility:
const x = "global x";
function first() {
const x = "first x";
function second() {
const x = "second x";
console.log(x); // Which x?
}
second();
}
first(); // Output: "second x"
The output is "second x" because JavaScript finds x in the local scope first. It doesn't need to look further up the scope chain.
When a variable in an inner scope has the same name as a variable in an outer scope, the inner variable shadows (hides) the outer one:
const message = "global";
function showMessage() {
const message = "local"; // Shadows global message
console.log(message); // Output: "local"
}
showMessage();
console.log(message); // Output: "global"
Here's where it gets interesting. Because of lexical scope, a function "remembers" the scope in which it was created:
function createGreeter(greeting) {
// greeting is in this scope
return function(name) {
// This inner function has access to greeting
// because of lexical scope
console.log(`${greeting}, ${name}!`);
};
}
const sayHello = createGreeter("Hello");
const sayHi = createGreeter("Hi");
sayHello("Alice"); // Output: "Hello, Alice!"
sayHi("Bob"); // Output: "Hi, Bob!"
Each returned function "closes over" its own greeting variable. This is a closure in action!
An important point: the scope chain is determined when the function is defined, not when it's called:
const value = "global";
function outer() {
const value = "outer";
function inner() {
console.log(value); // Will always log "outer"
}
return inner;
}
const myFunc = outer();
// Even if we create a new value variable here
const value = "new global";
myFunc(); // Still outputs: "outer"
The inner function was defined inside outer, so it will always reference outer's value, regardless of where it's called.
The scope chain can be as deep as you need:
const level1 = "L1";
function a() {
const level2 = "L2";
function b() {
const level3 = "L3";
function c() {
const level4 = "L4";
// Can access all levels
console.log(level1); // "L1"
console.log(level2); // "L2"
console.log(level3); // "L3"
console.log(level4); // "L4"
}
c();
}
b();
}
a();
Lexical scope is perfect for creating configured functions:
function createMultiplier(multiplier) {
return function(number) {
return number * multiplier;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const quadruple = createMultiplier(4);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(quadruple(5)); // 20
Each function "remembers" its own multiplier value through lexical scope and closures.
Now that you understand lexical scope and the scope chain, you're ready to create your first practical closures. In the next lesson, we'll build several closure examples and explore common patterns.