AiTechWorlds
AiTechWorlds
University learning companion
JavaScript Complete 2026 · Closures & Lexical Scope
A feature looks correct on a developer’s large monitor but fails on a slow connection and a small screen. Use this topic to inspect structure, state, loading/error behaviour, accessibility and the user journey—not just the happy path.
Use this course-level map to place “Closures & Lexical Scope” in the larger system. It complements—not replaces—a topic-specific worked diagram.
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.
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"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); // ReferenceErrorThe inner function "closes over" count it carries a reference to the variable, not a copy of its value at the time of creation.
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 privatefunction 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 xfunction 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 afterfunction 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// 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 ✓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!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.
University learning companion
JavaScript Complete 2026 · Closures & Lexical Scope
Complete the steps without looking at the answer first; record evidence rather than only a final result.
These are primary or established learning references for this subject. Check the relevant chapter or specification for the exact topic and version.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises