Skip to main content
โš™๏ธintermediate

Closures

Closures let inner functions remember variables from their outer scope even after the outer function has returned. This enables powerful patterns like data privacy, memoization, and function factories.

What is a Closure?

A closure is a function that remembers the variables from its outer scope even after that outer function has finished executing.

hljs javascript
function makeCounter() {
  let count = 0; // this variable is "closed over"

  return function() {
    count++;
    return count;
  };
}

const counter = makeCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

makeCounter returns a function. That function has a closure over count โ€” it keeps a reference to count and can read and update it, even though makeCounter has already finished running.

How Closures Work

When a function is created, it captures a reference to the scope chain at that point. This isn't a copy of the values โ€” it's a live reference:

hljs javascript
function makeMultiplier(factor) {
  // factor is captured in the closure
  return (number) => number * factor;
}

const double = makeMultiplier(2);
const triple = makeMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

// Each closure has its own independent `factor`

Each call to makeMultiplier creates a new closure with its own factor. double and triple don't share state.

Data Privacy

Closures are JavaScript's way of creating private state โ€” variables that can't be accessed from outside:

hljs javascript
function createBankAccount(initialBalance) {
  let balance = initialBalance; // private

  return {
    deposit(amount) {
      if (amount > 0) balance += amount;
      return balance;
    },
    withdraw(amount) {
      if (amount > 0 && amount <= balance) {
        balance -= amount;
        return balance;
      }
      return "Insufficient funds";
    },
    getBalance() {
      return balance;
    },
  };
}

const account = createBankAccount(100);
console.log(account.getBalance()); // 100
account.deposit(50);
console.log(account.getBalance()); // 150
account.withdraw(30);
console.log(account.getBalance()); // 120
// console.log(account.balance);   // undefined โ€” private!

The Classic Loop Bug

This is one of the most famous closure gotchas:

hljs javascript
// โœ— Bug: using var
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i);
  });
}

funcs[0](); // 3 โ€” not 0!
funcs[1](); // 3 โ€” not 1!
funcs[2](); // 3 โ€” not 2!

Because var is function-scoped, all three functions share the same i. By the time they're called, the loop has ended and i === 3.

Fix 1 โ€” Use let (block-scoped):

hljs javascript
const funcs = [];
for (let i = 0; i < 3; i++) {
  funcs.push(function() {
    console.log(i);
  });
}
funcs[0](); // 0 โœ“
funcs[1](); // 1 โœ“
funcs[2](); // 2 โœ“

let creates a new binding for each iteration.

Fix 2 โ€” IIFE (older pattern):

hljs javascript
const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push((function(j) {
    return function() { console.log(j); };
  })(i)); // immediately invoked, capturing i as j
}

Memoization

Closures can cache expensive computation results:

hljs javascript
function memoize(fn) {
  const cache = {}; // private to this closure

  return function(...args) {
    const key = JSON.stringify(args);
    if (key in cache) {
      console.log("Cache hit!");
      return cache[key];
    }
    cache[key] = fn(...args);
    return cache[key];
  };
}

function slowSquare(n) {
  // imagine this is expensive
  return n * n;
}

const fastSquare = memoize(slowSquare);

console.log(fastSquare(10)); // computes: 100
console.log(fastSquare(10)); // Cache hit! 100
console.log(fastSquare(20)); // computes: 400
console.log(fastSquare(20)); // Cache hit! 400

Partial Application

Closures let you "pre-fill" arguments:

hljs javascript
function multiply(a, b) {
  return a * b;
}

function partial(fn, ...presetArgs) {
  return (...laterArgs) => fn(...presetArgs, ...laterArgs);
}

const double = partial(multiply, 2);
const triple = partial(multiply, 3);

console.log(double(5));  // 10
console.log(triple(5));  // 15
console.log(double(12)); // 24

IIFE โ€” Immediately Invoked Function Expression

An IIFE is a function defined and immediately called. It creates a private scope:

hljs javascript
const result = (function() {
  const private = "I'm private";
  return private.toUpperCase();
})();

console.log(result); // "I'M PRIVATE"
// private is not accessible here

IIFEs were used before ES modules to avoid polluting the global scope. Modern code uses ES modules instead, but you'll still see IIFEs in legacy codebases.

Closure in Event Handlers

Closures are everywhere in browser code:

hljs javascript
function setupButton(buttonId, message) {
  const button = document.getElementById(buttonId);
  button.addEventListener("click", function() {
    // This function closes over `message`
    alert(message);
  });
}

setupButton("btn1", "Hello from Button 1");
setupButton("btn2", "Hello from Button 2");
// Each button remembers its own message
โ–ถTry it yourself

Key Takeaways

  • A closure is a function that retains access to its outer scope after that scope has exited
  • Closures create private state โ€” variables inaccessible from outside
  • Each closure gets its own copy of the closed-over variables
  • The classic var + for loop bug is caused by sharing scope โ€” fix with let
  • Practical uses: counters, memoization, partial application, data privacy

Ready to test your knowledge?

Take a quiz on what you just learned.

Take the Quiz โ†’