Scope & Closures
Lexical scope, closures, the dreaded `this`.
Scope is the rule that decides which variables are visible where. Closures are what happens when a function remembers the scope it was born in, even after that scope has finished running. If that sounds magical, that's because it kind of is, and it's the single most powerful idea in JavaScript.
Lexical scope: visibility follows the code
"Lexical" just means "based on where it is written." Inner blocks can see outer variables, but not the other way around:
const outer = "I am outside";
function speak() {
const inner = "I am inside";
console.log(outer); // "I am outside" ✅ inner can see outer
console.log(inner); // "I am inside"
}
speak();
console.log(inner); // ReferenceError ❌ outer can't see innerlet/const are block scoped, var is not
if (true) {
let blockOnly = "hi";
var leaks = "oops";
}
console.log(leaks); // "oops" ← var ignores blocks
console.log(blockOnly); // ReferenceError ← let respects themlet and const everywhere. Predictable scope = fewer surprises.Closures: functions that remember
When a function is created, it captures a snapshot of every variable it can see. Even after the outer function returns, the inner function keeps that snapshot alive. That captured environment is a closure.
function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
const fresh = makeCounter();
fresh(); // 1 - each call gets its own countThe returned function still has access to count after makeCounter() has returned. That's the closure. count is private to that counter instance.
Closures are how you get private state
Before classes had real private fields, closures were the only way to hide data in JS. They're still the cleanest way for small helpers:
function makeUser(name) {
let _password = null;
return {
setPassword(p) { _password = p; },
check(p) { return p === _password; },
// _password is invisible from outside
};
}
const u = makeUser("Ada");
u.setPassword("secret");
u.check("wrong"); // false
u.check("secret"); // true
u._password; // undefined - truly hiddenThe famous var-in-loop gotcha
Pre-2015 JS code is littered with this bug. Run the following with var and every callback logs 3:
// Broken: var is function-scoped, so all callbacks share the SAME i
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 3, 3, 3
// Fixed: let is block-scoped, each iteration gets its own i
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
// Logs: 0, 1, 2Each iteration with let creates a fresh binding, so the closure captures a different i every time. With var, all three callbacks point at the same variable, which is 3 by the time they run.
When closures cause memory leaks
Closures keep their captured variables alive. If you accidentally capture something large in a long-lived function, that memory can't be freed. The classic shape:
function setupHandler() {
const hugeArray = new Array(1_000_000).fill("data");
// This handler closes over hugeArray, even though it doesn't use it
document.addEventListener("click", () => {
console.log("clicked");
});
}Build your own counter
Here's a working counter factory. Extend it: add a decrement method, a reset, and a get()that doesn't change the value.
Quiz
What is a closure?
Recap
- Lexical scope: a variable is visible everywhere inside the block it was declared in, and nowhere else.
let/constare block-scoped.varis function-scoped (avoid it).- A closure is a function plus the variables it captured. It keeps them alive after the outer scope returns.
- Use closures for private state, factories, and memoization.
- Closures keep memory in scope. Detach long-lived listeners and timers when you're done.