JavaScript Closures
A closure is a function that remembers the variables from its surrounding (outer) scope even after that outer function has finished executing. This is one of JavaScript's most powerful and distinctive features.
Real-life analogy: Imagine packing a backpack before leaving home. Even after leaving, the backpack still carries everything that was packed. A closure is like that backpack — it carries the variables from where it was created, no matter where it goes.
Understanding Closures Step by Step
To understand closures, it helps to first see what happens when a function is defined inside another function.
function outer() {
let message = "Hello from outer!";
function inner() {
console.log(message); // Accesses outer's variable
}
inner();
}
outer(); // Hello from outer!This works because inner has access to outer's scope. Now, what if inner is returned from outer and called later?
function outer() {
let message = "Hello from closure!";
function inner() {
console.log(message); // Still remembers message
}
return inner; // Return the function, not its result
}
let greetFn = outer(); // outer() runs and finishes
greetFn(); // Hello from closure!
// Even though outer() is done, inner() still has access to "message"The variable message is preserved in memory because inner still references it. This is a closure.
A More Practical Closure Example
function makeCounter() {
let count = 0;
return function() {
count++;
console.log("Count:", count);
};
}
let counter = makeCounter();
counter(); // Count: 1
counter(); // Count: 2
counter(); // Count: 3Each time counter() is called, it increments count. The variable count is private — it cannot be accessed or modified from outside. This is the foundation of data encapsulation in JavaScript.
Multiple Closures — Each Has Its Own Scope
If makeCounter is called twice, each resulting function gets its own independent count variable.
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
let counterA = makeCounter();
let counterB = makeCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1 — independent of counterA!
console.log(counterA()); // 3Closures with Parameters
function multiplier(factor) {
return function(number) {
return number * factor;
};
}
let double = multiplier(2);
let triple = multiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(double(8)); // 16Here, double and triple each remember their own value of factor from when they were created.
Closures for Data Privacy
Closures allow creating private variables that cannot be directly accessed from outside. This is useful for protecting sensitive data.
function bankAccount(initialBalance) {
let balance = initialBalance; // Private — not accessible outside
return {
deposit(amount) {
balance += amount;
console.log("Deposited:", amount, "| Balance:", balance);
},
withdraw(amount) {
if (amount > balance) {
console.log("Insufficient funds!");
} else {
balance -= amount;
console.log("Withdrawn:", amount, "| Balance:", balance);
}
},
getBalance() {
return balance;
}
};
}
let account = bankAccount(1000);
account.deposit(500); // Deposited: 500 | Balance: 1500
account.withdraw(200); // Withdrawn: 200 | Balance: 1300
console.log(account.getBalance()); // 1300
// Direct access is impossible:
// console.log(account.balance); // undefinedClosures in Loops — A Common Pitfall with var
A classic mistake with closures and loops involves var. Since var is function-scoped (not block-scoped), all iterations of a loop share the same variable.
// Problem with var
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 3, 3, 3 (not 0, 1, 2)
}, 1000);
}
// After 1 second: i is already 3 for all three callbacksFix 1: Use let Instead of var
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Prints 0, 1, 2 correctly
}, 1000);
}
// Each iteration gets its own "i" because let is block-scopedFix 2: Use a Closure Factory
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2 — captures j immediately
}, 1000);
})(i); // Immediately Invoked Function Expression (IIFE)
}IIFE — Immediately Invoked Function Expression
An IIFE is a function that is defined and executed immediately. It is often used with closures to create a private scope.
(function() {
let secret = "This is private";
console.log(secret); // This is private
})();
// console.log(secret); // Error — not accessible outsidePractical Example — Login Attempt Tracker
function loginTracker(maxAttempts) {
let attempts = 0;
return {
tryLogin(success) {
if (attempts >= maxAttempts) {
console.log("Account locked. Too many failed attempts.");
return;
}
if (success) {
console.log("Login successful after " + attempts + " failed attempt(s).");
attempts = 0;
} else {
attempts++;
console.log("Failed attempt " + attempts + " of " + maxAttempts);
}
}
};
}
let login = loginTracker(3);
login.tryLogin(false); // Failed attempt 1 of 3
login.tryLogin(false); // Failed attempt 2 of 3
login.tryLogin(false); // Failed attempt 3 of 3
login.tryLogin(false); // Account locked. Too many failed attempts.Key Points to Remember
- A closure is a function that retains access to variables from its outer (enclosing) function's scope
- Closures work because JavaScript functions remember where they were created, not where they are called
- Each closure gets its own copy of the outer function's variables
- Closures enable data privacy — variables inside closures cannot be accessed from outside
- Use
letinstead ofvarinside loops that involve closures to avoid shared-variable bugs - IIFE (Immediately Invoked Function Expression) creates an isolated private scope using closures
