Closures & Lexical Scope
Closures & Lexical Scope
Closures are one of the most powerful — and most misunderstood — features in JavaScript. A closure is what happens when a function remembers the variables from its surrounding scope, even after that scope has finished executing.
Lexical Scope
JavaScript uses lexical scope (also called static scope): a variable's availability is determined by where it's written in the code, not where it's called from.
const message = "outer";
function outer() {
const message = "middle";
function inner() {
const message = "inner";
console.log(message); // "inner" — closest scope wins
}
inner();
console.log(message); // "middle"
}
outer();
console.log(message); // "outer"
What Is a Closure?
A closure is created when a function is defined inside another function and the inner function references variables from the outer function:
function makeCounter() {
let count = 0; // this variable is "closed over"
return function() {
count++; // still has access to count
return count;
};
}
const counter = makeCounter();
counter(); // 1
counter(); // 2
counter(); // 3
// count is NOT accessible from outside
console.log(count); // ReferenceError
The inner function "closes over" count — it carries a reference to the variable, not a copy of its value at the time of creation.
Practical Closures
Private State
function createBankAccount(initialBalance) {
let balance = initialBalance; // private
return {
deposit(amount) {
if (amount > 0) balance += amount;
return balance;
},
withdraw(amount) {
if (amount > balance) throw new Error("Insufficient funds");
balance -= amount;
return balance;
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.getBalance(); // 1500
account.balance; // undefined — truly private
Factory Functions
function makeAdder(x) {
return (y) => x + y; // closes over x
}
const add5 = makeAdder(5);
const add10 = makeAdder(10);
add5(3); // 8
add10(3); // 13
add5(7); // 12
// Each closure has its own x
Memoization
function memoize(fn) {
const cache = new Map(); // closed over by the returned function
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
const fib = memoize(function(n) {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
});
fib(40); // fast — computed once, cached after
Event Handlers with Context
function setupButton(buttonId, message) {
const btn = document.getElementById(buttonId);
btn.addEventListener("click", () => {
// 'message' is closed over — remembered when clicked
alert(message);
});
}
setupButton("btn1", "Hello!");
setupButton("btn2", "Goodbye!");
// Each button has its own closure with different message
The Classic Loop Closure Bug
// Bug: all callbacks share the same 'i'
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 3, 3, 3 (not 0, 1, 2)
// Why: var is function-scoped, 'i' is the same variable, already = 3 by the time callbacks run
// Fix 1: Use let (block-scoped — creates a new 'i' per iteration)
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Prints: 0, 1, 2 ✓
// Fix 2: Use an IIFE to capture the value (pre-ES6)
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(() => console.log(j), 100);
})(i);
}
// Prints: 0, 1, 2 ✓
Module Pattern
Closures enable the module pattern — encapsulating private state and exposing a public API:
const userStore = (function() {
// Private
const users = new Map();
let nextId = 1;
function validateUser(user) {
if (!user.name) throw new Error("Name required");
if (!user.email?.includes("@")) throw new Error("Valid email required");
}
// Public API
return {
add(user) {
validateUser(user);
const id = nextId++;
users.set(id, { ...user, id });
return id;
},
get(id) {
return users.get(id);
},
list() {
return [...users.values()];
},
count() {
return users.size;
}
};
})(); // IIFE — immediately invoked
userStore.add({ name: "Alice", email: "alice@example.com" });
userStore.count(); // 1
userStore.users; // undefined — private!
Closure and Memory
Closures keep referenced variables alive — which can cause memory leaks if you're not careful:
// Potential memory leak — large array stays in memory
function setup() {
const bigData = new Array(1000000).fill("x");
document.getElementById("btn").addEventListener("click", () => {
// This closure captures bigData — it won't be garbage collected
// as long as the event listener exists!
console.log(bigData.length);
});
}
// Fix: only close over what you need
function setup() {
const bigData = new Array(1000000).fill("x");
const dataLength = bigData.length; // capture the small value
document.getElementById("btn").addEventListener("click", () => {
console.log(dataLength); // only captures a number, not the array
});
// bigData can now be garbage collected after setup() returns
}
Next lesson: Error Handling with try/catch — defensive programming in JavaScript.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises