JavaScript Closures — A Simple Deep Dive
TL;DR
A closure is when a function remembers variables from its outer scope even after that outer function has finished running.
Think: “The inner function has a backpack with variables it captured when it was created.”
The Core Idea (Lexical Scope)
- JavaScript uses lexical (static) scoping: what you can access is determined by where functions are defined in the code (not where they’re called).
- When you create a function, JS binds a hidden link to its Lexical Environment (scope chain). That link is carried with the function forever → this is the closure.
function makeCounter() {
let count = 0; // captured variable
return function () { // inner function forms a closure over `count`
count++;
return count;
};
}
const next = makeCounter();
console.log(next()); // 1
console.log(next()); // 2 (still remembers `count`)
The makeCounter
frame is gone, but the inner function keeps count
alive through its closure.
Why Closures Are Useful
1.Data privacy / encapsulation
function createBankAccount() {
let balance = 0;
return {
deposit(x) { balance += x; },
withdraw(x) { if (x <= balance) balance -= x; },
getBalance() { return balance; }
};
}
const acc = createBankAccount();
acc.deposit(100);
console.log(acc.getBalance()); // 100 (no direct write access)
2.Callbacks & async
function greetLater(name) {
setTimeout(() => console.log("Hello", name), 500); // `name` is closed over
}
3.Function factories / partial application
const by = key => (a, b) => (a[key] > b[key] ? 1 : -1);
users.sort(by("age"));
4.Memoization
function memoize(fn) {
const cache = new Map();
return x => cache.has(x) ? cache.get(x) : (cache.set(x, fn(x)), cache.get(x));
}
Common Gotchas
1) Loops with var
vs let
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log("var:", i), 0);
}
// var: 4, var: 4, var: 4 (one shared `i`)
for (let j = 1; j <= 3; j++) {
setTimeout(() => console.log("let:", j), 0);
}
// let: 1, let: 2, let: 3 (new binding per iteration)
var
is function-scoped → the samei
is captured.let
/const
are block-scoped → each iteration creates a fresh binding.
2) Stale closures
function makeAdder(n) {
return x => x + n; // captures `n` at creation time
}
const add10 = makeAdder(10);
console.log(add10(5)); // 15
// If `n` changes later in outer scope (different ref), this `add10` still uses the captured one.
3) Memory considerations
- Closures keep referenced variables alive. Large objects in closures can extend memory lifetime.
- Avoid capturing unnecessary objects; null them out or restructure if needed.
Visualizing the Closure (Mental Model)
- When
function () { ... }
is created, JS binds a hidden pointer to its outer environment. - When the outer function returns, that environment may stay in memory if any inner function still references it.
- Each call to the outer function creates a new environment (so independent counters, etc.).
Patterns & Recipes
Module pattern
const counterModule = (function () {
let value = 0;
return {
inc() { value++; },
dec() { value--; },
get() { return value; }
};
})();
Once-only
function once(fn) {
let called = false, result;
return (...args) => {
if (!called) { called = true; result = fn(...args); }
return result;
};
}
Event handler factories
function makeHandler(prefix) {
return e => console.log(prefix, e.target.id);
}
button.addEventListener("click", makeHandler("Clicked:"));
Closures in Frameworks
React
- Every render creates a new function with its own closed-over values.
- Handlers can capture stale state. Prefer functional updates when next state depends on previous:
setCount(c => c + 1);
useRef
holds a stable object and does not trigger rerenders; useful to avoid re-creating closures around changing values.
Node.js
- Middleware factories (Express), curried config functions, and async flows rely heavily on closures.
Interview-Style Questions
1.Define a closure.
A function together with its lexical environment, allowing it to remember variables from its defining scope.
2.Why does var
+ setTimeout in a loop log the same value? How to fix?
Because a single var
binding is shared. Fix with let
or an IIFE:
for (var i = 1; i <= 3; i++) {
(i => setTimeout(() => console.log(i), 0))(i);
}
3.How do closures enable privacy?
By keeping variables in a scope that’s not directly exposed, only through returned functions.
4.Can closures cause memory leaks?
They can extend lifetimes. If you unintentionally retain references (e.g., listeners), memory can grow. Clean up listeners and avoid capturing heavy objects unnecessarily.
Quick Exercises
1.Create a counter with reset
function counter(start = 0) {
let n = start;
return {
next: () => ++n,
reset: () => (n = 0)
};
}
2.Build a simple rate limiter
function rateLimit(fn, ms) {
let ready = true;
return (...args) => {
if (!ready) return;
ready = false;
fn(...args);
setTimeout(() => (ready = true), ms);
};
}
3.Once with error handling
function onceSafe(fn) {
let called = false;
return (...args) => {
if (called) throw new Error("Already called");
called = true;
return fn(...args);
};
}
Summary
- Closure = function + its lexical environment.
- They power privacy, factories, async callbacks, and more.
- Watch for loop scoping, stale closures, and unnecessary memory retention.
✨ Master closures, and a lot of JS “magic” becomes predictable.