Closures

Med

A closure is a function that remembers the variables from its outer scope even after the outer function has returned. Think of it as a function carrying a "backpack" of variables wherever it goes.

Interactive Visualization

CodeCreation Phase
1function outer() {
2 let x = 10;
3 function inner() {
4 return x;
5 }
6 return inner;
7}
8
9const fn = outer();
10fn(); // 10
Call Stack
Global EC
Variables:
fnundefined
Heap Memory
(empty)
Output
Global EC created - outer function is defined
1 / 6
Key Insight: A closure is formed when an inner function is returned from an outer function, maintaining access to the outer function's variables even after the outer function has completed.

Understanding Closures

A closure is one of the most important concepts in JavaScript, and understanding it deeply is essential for writing clean, modular code. At its core, a closure is created when a function retains access to variables from its surrounding lexical scope, even after that outer function has finished executing. Every time you define a function inside another function, you create a closure.

The reason closures work is tied to how JavaScript manages scope. When a function is created, it captures a reference to the variables in its enclosing environment — not a snapshot of their values. This means the inner function can read and modify those variables long after the outer function has returned. Think of it as the function carrying an invisible backpack of all the variables it might need.

Closures enable several powerful patterns in JavaScript. The most common is data privacy — by wrapping variables inside a function and only exposing controlled access through returned methods, you can create private state that cannot be tampered with from outside. This is the foundation of the module pattern, which was the standard way to organize JavaScript code before ES modules existed.

Another practical use is creating function factories. A factory function takes configuration parameters and returns a new function that has those parameters baked in via closure. This is how techniques like partial application and currying work. Memoization, where a function caches its results for repeated inputs, also relies on closures to maintain the cache between calls.

The classic closure interview question involves closures inside loops. When using var in a for loop with setTimeout, all callbacks share the same variable and print the final value. The fix is to use let (which creates a new binding per iteration) or to wrap the callback in an IIFE that captures the current value. Understanding why this happens — captured by reference, not by value — is the key insight interviewers are testing for.

One thing to be mindful of is memory. Since closures keep references to outer variables alive, those variables cannot be garbage collected as long as the closure exists. In most cases this is harmless, but if a closure accidentally captures a large object or DOM element, it can cause memory leaks. Removing event listeners and nullifying references when they are no longer needed helps prevent this.

Key Points

  • A closure is created every time a function is created
  • Inner functions have access to outer function variables
  • The outer variables are captured by reference, not by value
  • Closures enable data privacy and stateful functions

Code Examples

Basic Closure

function outer() {
  let count = 0;

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

const counter = outer();
counter();  // 1
counter();  // 2
counter();  // 3

Inner function remembers outer variables

Counter Example

function createCounter(start) {
  let count = start;

  return {
    increment: () => ++count,
    decrement: () => --count,
    getValue: () => count
  };
}

const counter = createCounter(10);
counter.increment();  // 11
counter.increment();  // 12
counter.getValue();   // 12

Each call uses the same closed-over variable

Private Variables

function createAccount(initial) {
  let balance = initial;  // private!

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

const account = createAccount(100);
account.deposit(50);   // 150
account.balance;       // undefined (private!)

Closures enable data privacy

Multiple Closures

function createCounter(start) {
  let count = start;
  return () => count++;
}

const counterA = createCounter(0);
const counterB = createCounter(100);

counterA();  // 0
counterA();  // 1
counterB();  // 100
counterB();  // 101

// Each closure has independent state

Each closure has its own independent state

Loop Closure Bug

// THE BUG: All callbacks share same i
for (var i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// Output: 3, 3, 3

// Why? var is function-scoped
// All 3 closures reference the
// SAME i, which is 3 after loop

Classic var loop closure problem

Loop Fix with let

// THE FIX: let creates new binding
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 100);
}
// Output: 0, 1, 2

// Why? let is block-scoped
// Each iteration gets its own i
// Each closure captures different i

let creates new binding per iteration

Common Mistakes

  • Loop variable capture - all callbacks share the same variable
  • Memory leaks from closures holding large objects
  • Forgetting that closures capture by reference, not value

Interview Tips

  • Use the "backpack" metaphor to explain closures
  • Show practical uses: data privacy, function factories, memoization
  • Be ready to solve the classic loop closure problem

Related Concepts