beginner

JavaScript Closures Explained with Interview Examples

What closures are, how lexical scope works, the classic var-in-a-loop question, and how closures power memoize, once, and module patterns in interviews.


What a closure actually is

A closure is a function bundled with the lexical scope it was created in. When a function references a variable from an enclosing scope, JavaScript keeps that variable alive for as long as the function exists — even after the outer function has returned.

function makeCounter() {
  let count = 0;            // lives on after makeCounter returns
  return function () {
    count += 1;
    return count;
  };
}
 
const counter = makeCounter();
counter(); // 1
counter(); // 2
 
const other = makeCounter();
other();   // 1 — each call to makeCounter creates a fresh scope

Two things interviewers listen for in your explanation:

  1. Closures capture variables, not values. The inner function sees the current state of count, not a snapshot from creation time.
  2. Each invocation of the outer function creates a new scope. counter and other are independent because they close over different count variables.

The classic question: var in a loop

This is the most-asked closure question in existence:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// 3, 3, 3

Why? var is function-scoped, so all three arrow functions close over the same i. By the time the timers fire (after the loop, per the event loop), i is 3.

Interviewers then ask you to fix it — know all three fixes and their trade-offs:

// 1. let — block scoping creates a fresh binding per iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 0, 1, 2
}
 
// 2. IIFE — capture the value in a new function scope (pre-ES6 style)
for (var i = 0; i < 3; i++) {
  ((j) => setTimeout(() => console.log(j), 0))(i);
}
 
// 3. setTimeout's extra arguments — least known, most elegant
for (var i = 0; i < 3; i++) {
  setTimeout(console.log, 0, i);
}

Closures are the engine of this whole catalog

Most implementation questions are secretly closure questions. If you can articulate where the state lives, the implementations follow:

  • once() — closes over a called flag and a cached result.
  • memoize() — closes over a cache Map.
  • debounce / throttle — close over a timer id and timestamps.
  • curry — closes over the arguments collected so far.

A compact example that interviewers love because it combines two of these:

function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      called = true;
      result = fn.apply(this, args);
    }
    return result;
  };
}

The returned function is the closure: called and result are invisible to callers, impossible to tamper with, and garbage-collected only when the function itself is.

Encapsulation: the module pattern

Before class fields and #private names, closures were JavaScript's only privacy mechanism — and the pattern still appears in interviews:

function createBankAccount(initial) {
  let balance = initial;               // truly private
 
  return {
    deposit(amount) { balance += amount; return balance; },
    withdraw(amount) {
      if (amount > balance) throw new Error("Insufficient funds");
      balance -= amount;
      return balance;
    },
    getBalance() { return balance; },
  };
}
 
const account = createBankAccount(100);
account.deposit(50);      // 150
account.balance;          // undefined — no way to reach it directly

Memory: the follow-up that separates seniors

Closures keep scopes alive, which means they can leak memory when a long-lived function captures a large object it no longer needs:

function attach(bigData) {
  // handler closes over bigData forever, even if it only needs the length
  document.addEventListener("click", () => console.log(bigData.length));
}

The fix is to capture only what you need (const len = bigData.length) or to remove listeners when done. Strong answers mention that modern engines can optimize what a closure retains, but you shouldn't rely on it — captured objects referenced anywhere in the inner function are kept.

Interview follow-ups to be ready for

  • "What's the difference between scope and closure?" — scope is the visibility rule; a closure is a function retaining its scope after the outer function exits.
  • "Do arrow functions create closures?" — yes; they close over variables (and this, lexically — see the this keyword).
  • "Can two closures share state?" — yes, if they're created in the same scope (like deposit and withdraw above); that's the basis of the module pattern.

  • The Event Loop

    intermediate

    How the call stack, microtask queue, and macrotask queue decide execution order — with the output-prediction questions interviewers love.

  • The this Keyword

    intermediate

    The four binding rules, arrow-function behavior, and the lost-this bugs that call, apply, and bind exist to fix.

  • The prototype chain, __proto__ vs prototype, and what class syntax actually does under the hood.