Closures

Course: JavaScript Advanced

Lesson 2: Closures

Level

Advanced

Estimated Reading Time

10–15 minutes

Goal

By the end of this lesson, the learner will be able to:

  • Explain what a closure is and why it forms
  • Use closures to create private state and factory functions
  • Recognize practical closure patterns used in real frontend code

Frontend Development Context

Closures are one of the most powerful and frequently used features in JavaScript, even when developers don't realize they're using them. Every time you write an event listener that references a variable from the surrounding function, or create a counter that remembers its value between calls, you are using closures. They are the mechanism behind module patterns, memoization, currying, and most state-encapsulation techniques used before React hooks existed.

In modern frontend development, closures appear in custom hooks, debounce implementations, factory functions for creating reusable UI components, and anywhere you need to preserve private state without exposing it globally. A clear understanding of closures lets you write encapsulated, leak-free code and recognize when unexpected variable capture is causing bugs in loops or async callbacks.

Explanation

A closure is created when a function retains access to its lexical scope even after the outer function that defined it has returned. In JavaScript, functions don't just capture a snapshot of a value — they maintain a live reference to the variable environment of the scope in which they were created.

When an inner function is returned from or outlives an outer function, the JavaScript engine keeps that outer scope's variable environment alive in memory as long as the inner function exists. This is the closure: the combination of the function and the environment it was created in.

The most practical use of closures is private state. By defining variables inside a function and returning an object or function that can read and modify those variables, you create state that cannot be accessed or tampered with from outside. This is the foundation of the module pattern and was the primary way to achieve encapsulation in JavaScript before ES6 classes.

Factory functions are another major closure use case. A factory function is a function that produces and returns new functions or objects with their own private state. Each call to the factory creates a new closure, so each returned function has its own independent variable environment. This lets you create multiple independent instances without classes.

One important subtlety is that closures capture variable bindings, not values. If you close over a variable that later changes, the function sees the updated value. This is why closures in loops with var behave unexpectedly — all iterations share the same binding. Using let (which creates a new binding per iteration) or wrapping in an IIFE fixes this.

Code Example

This example builds a counter factory that uses closures to maintain private state, and a memoization function that caches results using a closure over a Map.

// Factory function — each call creates a new independent closure
function makeCounter(start = 0, step = 1) {
  let count = start;

  return {
    increment() { count += step; return count; },
    decrement() { count -= step; return count; },
    reset()     { count = start; return count; },
    value()     { return count; },
  };
}

const pageViews = makeCounter(0, 1);
const cartItems = makeCounter(0, 1);

pageViews.increment(); // 1
pageViews.increment(); // 2
cartItems.increment(); // 1 — independent state

console.log(pageViews.value()); // 2
console.log(cartItems.value()); // 1

// Memoization using closure over a Map
function memoize(fn) {
  const cache = new Map();

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      console.log("cache hit:", key);
      return cache.get(key);
    }
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}

function slowSquare(n) {
  return n * n;
}

const fastSquare = memoize(slowSquare);
console.log(fastSquare(9));  // 81 — computed
console.log(fastSquare(9));  // 81 — from cache
console.log(fastSquare(12)); // 144 — computed

Code Explanation

makeCounter is a factory function. Each call creates a new execution context with its own count and start variables. The returned object's methods — increment, decrement, reset, value — form closures over that specific execution context. When pageViews.increment() runs, it reads and updates the count binding from pageViews's own closure, completely separate from cartItems's closure. Neither counter can see or touch the other's private state.

The memoize function illustrates closure over a more complex piece of state: a Map. Every time memoize is called, it creates a new cache Map and returns a new function that closes over it. When fastSquare(9) is called the first time, there's no cache hit, so slowSquare(9) runs and the result is stored. The second call finds "[9]" in the cache and returns immediately without calling slowSquare again.

Notice that cache is completely private. There is no way for external code to read or tamper with it — it only exists inside the closure. This is exactly the same privacy model that module systems provide, just implemented with a plain function.

Both patterns here — the counter factory and memoize — are used directly in production code. React's useMemo and useCallback hooks use the same closure-based caching idea internally.

Pattern Highlights

Positive Pattern: Use factory functions with closures to create multiple independent instances of stateful logic without classes, keeping private state genuinely private and inaccessible from outside.

⚠️ Neutral Note: Closures keep their outer scope's variable environment alive in memory. If you create many closures that each hold large data structures, you can create significant memory pressure — be deliberate about what each closure captures.

Negative Pattern: Do not close over a loop variable declared with var expecting each iteration to capture a unique value — all closures share the same var binding and will see the final value after the loop ends.

Common Mistakes

Mistake 1: Closing over var in a loop

A classic bug: attaching event listeners in a loop with var means every handler references the same variable.

for (var i = 0; i < 3; i++) {
  buttons[i].addEventListener("click", function () {
    console.log("Button", i); // always logs 3, never 0, 1, or 2
  });
}

By the time any click fires, the loop has finished and i is 3. Fix this by using let (which creates a new binding per iteration) or by capturing the value with an IIFE or bind.

Mistake 2: Accidentally sharing state across instances

If the private variable is defined outside the factory, all instances share it instead of each having their own.

let count = 0; // shared across all counters!

function makeCounter() {
  return {
    increment() { count++; },
    value() { return count; },
  };
}

const a = makeCounter();
const b = makeCounter();
a.increment();
console.log(b.value()); // 1 — b sees a's changes

The variable must be declared inside the factory function so each call creates a fresh binding.

Mistake 3: Holding closures over large objects unnecessarily

Closures keep their entire captured scope alive, including objects you might not need anymore.

function processData(largeArray) {
  const summary = largeArray.reduce((acc, item) => acc + item.value, 0);

  // This closure holds largeArray alive even though only summary is needed
  return function getReport() {
    console.log(largeArray, summary);
  };
}

If getReport only needs summary, restructure the closure to not reference largeArray. This lets the garbage collector reclaim that memory once processData returns.

Quick Recap

  • A closure is the combination of a function and the lexical environment it was created in; the function retains live access to those variable bindings even after the outer function returns.
  • Factory functions use closures to give each returned instance its own private state — a powerful alternative to classes for simple encapsulation.
  • Closures capture variable bindings, not values — mutating a closed-over variable affects all functions sharing that closure.
  • Common closure pitfalls include var in loops, accidentally shared state, and holding large objects in memory longer than necessary.

Ready to test your understanding? Take the quiz to reinforce what you learned.

© 2026 Ant Skillsv.26.07.07-23:01