Scope & Closures
Where variables live, how functions remember their surroundings, and the closure pattern that powers callbacks and modules.
Where variables exist — and how functions remember their home
Scope — the "visibility zone" of a variable
Every variable has a scope — the region of code where it exists. Outside that region, the variable doesn't exist, and trying to use it causes an error.
// Global scope — declared outside any function or block
const appName = "My App"; // visible everywhere in this file
function showName() {
// Function scope — declared inside a function
const localGreeting = "Hello"; // only visible inside showName()
console.log(localGreeting + " from " + appName); // both visible here
}
showName(); // "Hello from My App"
// console.log(localGreeting); // ✗ ReferenceError — doesn't exist out here// Block scope — let/const declared inside { } stay inside { }
if (true) {
let insideIf = "I only exist in here";
const alsoInside = "Me too";
console.log(insideIf); // ✓ works
}
// console.log(insideIf); // ✗ ReferenceError
for (let i = 0; i < 3; i++) {
// i only exists inside this loop
}
// console.log(i); // ✗ ReferenceErrorThe scope chain — looking things up
When JavaScript looks for a variable, it starts in the current scope, then looks in the parent scope, then its parent, all the way up to global scope. This is the scope chain.
const greeting = "Hello"; // global
function outer() {
const name = "Alice"; // outer's scope
function inner() {
const punctuation = "!"; // inner's scope
// inner can see ALL of these:
console.log(greeting + ", " + name + punctuation); // "Hello, Alice!"
}
inner();
// outer can't see punctuation — it's in inner's scope
}
outer();Closures — a function that remembers its home
A closure happens when a function is defined inside another function and keeps a reference to the outer function's variables — even after the outer function has finished running.
function makeGreeter(greeting) {
// When makeGreeter finishes, 'greeting' would normally disappear.
// But the returned function CLOSES OVER it — keeps it alive.
return function(name) {
console.log(`${greeting}, ${name}!`); // still has access to greeting
};
}
const sayHello = makeGreeter("Hello");
const sayNamaste = makeGreeter("Namaste");
sayHello("Alice"); // "Hello, Alice!"
sayNamaste("Bob"); // "Namaste, Bob!"
sayHello("Charlie"); // "Hello, Charlie!" — greeting is still "Hello"Think of a closure as a function with a backpack. When it leaves the place where it was born, it takes a copy of the surrounding variables with it in that backpack.
The loop + setTimeout trap (and how closures explain it)
// Classic trap with var — all callbacks share the same i:
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 — by the time the callbacks run, loop is done and i=3
// Fix 1: use let — creates a new i for each iteration
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 ✓
// Fix 2: closure to capture the current value (pre-ES6 style)
for (var i = 0; i < 3; i++) {
(function(j) { // j is a new variable for each iteration
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2 ✓let creates a fresh binding
for each loop iteration. var shares one binding across all iterations.
Use let in loops, always.
Official Sources
- MDN Web Docs — Closures
- MDN Web Docs — Scope
- ECMAScript 2024 — Environment Records (§9.1)
- MDN Web Docs — var scoping and the loop problem
The Codex links only to official documentation and the ECMAScript specification. No third-party tutorials.