Bitlyst

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 same i 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.

How did you like this post?

👍0
❤️0
🔥0
🤔0
😮0
JavaScript Closures — A Simple Deep Dive · Bitlyst