Closures Scope and Memory What Really Happens Under the Hood

🟦 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?

  • var is 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:

  • count stays 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