Javascript Closures

JS Example

let createCounter = function(n) {
    let theCounter = n;
    return function() {
      theCounter++;
      return theCounter;
    };
};


const counter1 = createCounter(10)

console.log(counter1()); 
//prints 11
console.log(counter1()); 
//prints 12

const counter2 = createCounter(20)

console.log(counter2()); 
//prints 21

ES6 Version

function createCounter(start = 0) {
  let count = start;

  return () => {
    count++;
    return count;
  };
}

Each call to createCounter(...) produces a small function that remembers the value theCounter. Calling the returned function increases that remembered value and returns it. 

counter1 and counter2 each keep their own remembered theCounter.

It is sometimes helpful to think of a closure as a function plus a little backpack of variables it can see from where it was created (its lexical environment).

  • createCounter returns the inner function. That inner function still has a reference to the environment where theCounter equals 10. We store the returned function in counter1.

The invocation of createCounter has finished, but the environment is not removed because the returned function still points to it. The function carries the environment with it.

Why can this be so confusing?

  1. Invisible state — functions usually look like: input → output. Closures add hidden memory: the function now also depends on variables it “remembers”. That hidden state makes behavior less obvious at a glance.
  2. Life after returning — normally local variables disappear when a function returns. With closures the locals persist because a returned inner function keeps them alive. That violates many people’s first mental model of “locals get cleared”.
  3. Shared vs separate environments — sometimes people expect every function to get a brand new copy of variables; other times a variable is shared between many functions. That nuance causes bugs (especially in loops).
  4. Mutation and unexpected memory retention — closures can keep large objects in memory unexpectedly (memory leak risk) or can be mutated from multiple places.

The pattern below makes the API clearer (since you can see the methods that operate on the hidden state).

function createCounter(n) {
  let theCounter = n;
  return {
    increment() { theCounter++; return theCounter; },
    get() { return theCounter; }
  };
}

const c = createCounter(5);
console.log(c.get());      // 5
console.log(c.increment()); // 6

FINALLY! (the loop / var problem)

A famous confusing example:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 10);
}
// likely prints: 3 3 3
for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 10);
}
// prints: 0 1 2

Why? var i is function-scoped, so each loop iteration uses the same i. The timeout callbacks all share that single i, and by the time they run, i is 3.

let creates a fresh binding for each iteration, so each closure captures a distinct i.


Quick diversion on what function-scoped means:

  • If var appears inside a loop → the whole function owns that variable
    (not just the loop block)
  • If var appears inside an if → the whole function owns it
    (not just the if block)
  • If there is no surrounding function → it becomes a global variable

This is different from let and const, which are block-scoped (they belong to the nearest { ... } block).


And why is this an example of a closure?

Each function() passed to setTimeout closes over the variable i.

Meaning:

  • The callback function remembers the variable i from its surrounding scope (the loop’s outer function or the global scope).
  • That variable is not copied — it is referenced.
  • All the callbacks refer to the same i, because var creates a single binding for the entire function, not one per loop iteration.

This is directly what a closure is:

A function that “remembers” and retains access to variables from its outer scope even after that scope has finished executing.


Short recap

  • The original code works because each call to createCounter(n) creates a distinct environment containing theCounter, and the returned function closes over (remembers) that variable.
  • Closures are powerful (private state, factories, callbacks) but confusing because they introduce hidden, persistent state and subtle sharing rules.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *