Closures

A closure is a function that keeps access to variables from the scope where it was created, even after that scope has finished.

A closure, step by step

function makeCounter() {
  let count = 0;

  return function increment() {
    count += 1;
    return count;
  };
}

const counter = makeCounter();

console.log(counter()); // 1
console.log(counter()); // 2
  1. makeCounter() runs.

    A new local binding named count is created.

  2. It returns increment.

    The inner function closes over the binding, not a frozen copy of its value.

  3. The outer call finishes.

    Normally its locals could be discarded, but increment still references count.

  4. Each call updates the same binding.

    That is why the results progress from 1 to 2.

Each outer call creates new state

Calling the factory twice creates two separate lexical environments. The counters do not interfere with each other.

const likes = makeCounter();
const shares = makeCounter();

likes();  // 1
likes();  // 2
shares(); // 1, its own count

What closures are good for

Private state

Expose a small public API while keeping the underlying value inaccessible from the outside.

function createWallet(startingBalance) {
  let balance = startingBalance;

  return {
    deposit(amount) { balance += amount; },
    getBalance() { return balance; }
  };
}

const wallet = createWallet(100);
wallet.deposit(50);
wallet.getBalance(); // 150
wallet.balance;      // undefined

Function configuration

Create specialized functions without repeatedly passing the same input.

function multiplyBy(factor) {
  return number => number * factor;
}

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

double(8); // 16
triple(8); // 24

Callbacks and event handlers

A callback can retain context from the code that registered it.

function setupButton(button, message) {
  button.addEventListener("click", () => {
    console.log(message);
  });
}

setupButton(saveButton, "Saved!");
// The handler remembers message after setupButton returns.

Debouncing

Delay a function until calls stop arriving. The returned function closes over timerId, so every call can cancel the previous timer.

function debounce(fn, delay) {
  let timerId;

  return function (...args) {
    clearTimeout(timerId);

    timerId = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
}

const search = debounce(query => {
  console.log(query);
}, 300);

search("j");
search("ja");
search("javascript"); // only this call runs

Throttling

Limit a function to one call per interval. The returned function closes over ready, which records whether another call is allowed.

function throttle(fn, interval) {
  let ready = true;

  return function (...args) {
    if (!ready) return;

    ready = false;
    fn.apply(this, args);

    setTimeout(() => {
      ready = true;
    }, interval);
  };
}

const trackScroll = throttle(() => {
  console.log("scroll position recorded");
}, 200);

The classic loop trap

var creates one function-scoped binding, so every callback shares it. let creates a fresh binding for each iteration.

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

// 3, 3, 3
A binding per iteration
for (let i = 0; i < 3; i++) {
  setTimeout(() => {
    console.log(i);
  }, 0);
}

// 0, 1, 2

The callbacks run after the loop. By then the single var i binding contains 3; the three let i bindings still contain 0, 1, and 2.

Closures and memory

A closure keeps referenced values reachable. This is useful, but long-lived listeners, timers, or caches can unintentionally retain large objects.

Check your understanding

Predict each result before opening the answer.

  1. What does this print?

    let label = "before";
    
    function showLabel() {
      console.log(label);
    }
    
    label = "after";
    showLabel();
    Show answer

    "after". The closure references the label binding, not the value it held when the function was defined.

  2. What are the two results?

    function makeAdder(amount) {
      return value => value + amount;
    }
    
    const addFive = makeAdder(5);
    const addTen = makeAdder(10);
    
    addFive(2);
    addTen(2);
    Show answer

    7 and 12. Each call to makeAdder creates a new amount binding, so each returned function closes over different state.

  3. Can outside code set secret directly?

    function createSecret() {
      let secret = "orbit";
      return () => secret;
    }
    
    const reveal = createSecret();
    Show answer

    No. secret is outside the caller's scope. Only the returned function has access to it, which makes closure-based data privacy possible.