🟦 JavaScript Deep Dive — Issue #7
Closures, Scope & Memory: What Really Happens Under the Hood
Why closures are powerful, dangerous, and often misunderstood
Closures are one of JavaScript’s greatest superpowers — and one of its most common sources of bugs, memory leaks, and confusion.
Most developers use closures.
Far fewer truly understand what they capture, how long they live, and why they sometimes cause performance problems.
This issue clears that up — for good.
🧠 Why Closures Matter More Than You Think
Closures affect:
- Memory usage
- Garbage collection
- Performance
- Async behavior
- React hooks
- Event handlers
- Module patterns
- Data privacy
If you understand closures deeply, entire categories of bugs simply disappear.
🟨 1. What a Closure Actually Is (Not the Buzzword Version)
A closure is created when:
A function remembers variables from its lexical scope, even after that scope has finished executing.
Example:
function outer() {
const secret = "hidden";
return function inner() {
console.log(secret);
};
}
const fn = outer();
fn(); // "hidden"
outer() is done — but secret is still alive.
Why?
Because inner() closed over it.
🟨 2. Lexical Scope vs Execution Context
Understanding closures requires separating two concepts:
Lexical Scope
- Defined at write time
- Based on where functions are written in code
- Never changes
Execution Context
- Created at run time
- Determines what’s currently executing
- Comes and goes
Closures use lexical scope, not execution order.
This explains why moving code around can break logic even if it “looks the same.”
🟨 3. Closures and Memory: What Stays Alive
Anything referenced by a closure cannot be garbage collected.
Example:
function create() {
const bigData = new Array(1_000_000).fill("*");
return () => bigData.length;
}
Even if you only need the length, the entire array stays in memory.
⚠️ Common mistake:
Accidentally closing over large objects.
Best practice:
Close over only what you need.
🟨 4. The Classic Loop Bug (And Why It Happens)
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
Output:
3
3
3
Why?
varis function-scoped- All closures reference the same
i - By the time callbacks run,
i === 3
Fix with let:
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0);
}
Each iteration creates a new binding.
🟨 5. Closures in Event Handlers
function setup() {
let count = 0;
button.addEventListener("click", () => {
count++;
console.log(count);
});
}
This looks harmless — but:
countstays in memory- Listener keeps the closure alive
- Removing the DOM node without removing the listener leaks memory
Best practice:
Always clean up event listeners.
🟨 6. Closures + Async = Subtle Bugs
Closures don’t capture values — they capture references.
let status = "idle";
async function run() {
await delay(1000);
console.log(status);
}
status = "done";
run(); // logs "done"
This surprises many developers.
Solution:
Capture the value explicitly.
const currentStatus = status;
🟨 7. Closures in React (Why Hooks Work)
Hooks rely entirely on closures.
useEffect(() => {
console.log(count);
}, []);
Why does this sometimes log stale values?
Because the effect closure captures the initial count.
Understanding closures explains:
- Stale state bugs
- Dependency arrays
- Why refs exist
🟨 8. Module Pattern: Closures for Data Privacy
const counter = (() => {
let value = 0;
return {
inc() { value++; },
get() { return value; }
};
})();
This pattern:
- Protects internal state
- Prevents accidental mutation
- Is still widely used (even with ES modules)
Closures enable true encapsulation in JavaScript.
🟨 9. Garbage Collection & Closure Lifetimes
Closures live as long as something references the function.
Common leak sources:
❌ Event listeners
❌ Timers
❌ Global references
❌ Cached callbacks
❌ Framework lifecycle mismatches
If a function survives — so does everything it closes over.
🧩 Mini Exercises
1. What does this log?
function makeFn() {
let x = 10;
return () => x++;
}
const f = makeFn();
f();
f();
2. Why is this a memory risk?
function cache() {
const data = fetchBigData();
return () => data;
}
3. Fix this closure bug:
for (var i = 0; i < 5; i++) {
handlers.push(() => i);
}
🟦 Closure Best Practices
✔ Prefer let / const
✔ Avoid closing over large objects
✔ Clean up event listeners
✔ Be careful with async + closures
✔ Understand how frameworks use closures
✔ Don’t fear closures — respect them
🏁 Final Thoughts
Closures aren’t magic — they’re a predictable result of JavaScript’s lexical scoping rules.
Once you understand:
- What’s captured
- What stays alive
- When memory is released
You gain far more control over your code’s correctness and performance.
Next issue:
👉 Issue #8 — JavaScript Architecture Patterns for Large Applications