# Closures — Part 2: Memory and Advanced Patterns

## 1. What a Closure Actually Keeps Alive

A closure does not selectively keep only the variables it references — in most engines it keeps the **entire lexical environment record** of the enclosing scope reachable. Two consequences:

```js
function outer() {
  const bigArray = new Array(1_000_000).fill("data"); // large
  const smallValue = 1;

  return function inner() {
    return smallValue; // only uses smallValue...
  };
}

const fn = outer(); // ...but in older/naive engines bigArray may be retained too,
                     // because inner and bigArray share the same environment record.
```

Modern V8 performs closure optimization and can discard genuinely unreferenced bindings, but you should not rely on this — **don't hold large objects in a scope that a long-lived closure shares**, if you can avoid it. Extract only what you need into a smaller scope first.

## 2. Module Pattern (Pre-ES-Modules Encapsulation)

Before ES modules existed, closures were the primary way to build private state and a public API from an IIFE:

```js
const Counter = (function () {
  let count = 0; // private, invisible outside this IIFE
  return {
    increment() { return ++count; },
    reset() { count = 0; },
  };
})();

Counter.increment(); // 1
Counter.increment(); // 2
Counter.reset();
```

## 3. Memoization

Closures let a function remember results of previous calls in a private cache:

```js
function memoize(fn) {
  const cache = new Map();
  return function (...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn(...args);
    cache.set(key, result);
    return result;
  };
}
```

`cache` is private to each memoized function instance — inaccessible and untamperable from outside.

## 4. Partial Application / Currying

```js
function multiply(a) {
  return function (b) {
    return a * b;
  };
}

const double = multiply(2);
double(5); // 10
```

`double` is a closure that "remembers" `a = 2` permanently.

## 5. Once-Only Execution

```js
function once(fn) {
  let called = false;
  let result;
  return function (...args) {
    if (!called) {
      result = fn(...args);
      called = true;
    }
    return result;
  };
}
```

`called` and `result` persist across calls to the returned function because of the closure, even though `once()` itself only runs a single time.

## 6. Closures vs Classes for Private State

| | Closures | Classes with `#private` |
|---|---|---|
| Privacy enforced | ✅ true (no access path exists) | ✅ true (since ES2022) |
| Shared state between instances | Manual (module-level variable) | Natural (per-instance fields) |
| Performance for many instances | Each closure duplicates its own functions | Methods shared on the prototype |
| Best for | Small, one-off stateful functions | Many instances of the same shape |

For a handful of stateful objects, closures are fine. For thousands of instances, class-based private fields are more memory-efficient because methods live once on the prototype instead of being recreated per closure.

## 7. Under the Hood: Context Objects, Precisely

A closure is not a copy of anything — mechanically, it is a function object holding a pointer to a `Context` object (see the Scope topic's "Under the Hood" section for the full Context-chain mechanics). Understanding this precisely resolves a lot of confusion about what closures "really" keep alive.

### Only captured variables get promoted to the heap

```js
function outer() {
  const captured = "kept alive";
  const notCaptured = "discarded when outer() returns";

  return function inner() {
    return captured; // only THIS variable forces a heap Context
  };
}
```

V8's compiler determines, per variable, whether any nested function references it. `captured` gets allocated into a heap `Context` object because `inner` reads it. `notCaptured` is never referenced by any inner function, so it stays a disposable stack slot and is gone the instant `outer()` returns — it costs nothing after that, regardless of how long `inner` (and its Context) survives.

### Why "a closure keeps its whole scope alive" is a useful simplification, not the literal mechanism

```js
function outer() {
  const bigArray = new Array(1_000_000).fill(0); // large
  const smallFlag = true;

  return function inner() { return smallFlag; }; // only touches smallFlag
}
```

If `bigArray` and `smallFlag` are declared in the *same block* and the compiler's analysis is conservative for that scope, both can end up in the *same* `Context` object (Contexts are typically allocated per-scope, not per-variable) — meaning `bigArray` rides along even though `inner` never touches it. This is the real, nuanced version of the "closures can leak memory" warning: the risk isn't per-variable, it's **per-scope** — anything sharing a block with a captured variable can be kept alive as a side effect of that Context's granularity. The practical fix from Part 1 (extract only what you need into a smaller, dedicated block before creating the closure) works specifically because it shrinks what that Context has to hold.

### One Context per call, not per function

```js
function counter() {
  let count = 0; // a NEW Context object is allocated on EVERY call to counter()
  return () => ++count;
}
const c1 = counter();
const c2 = counter();
```

`c1` and `c2` hold pointers to two entirely separate `Context` objects — this is why they track independent counts. Nothing is shared between them except the *shape* of the code; the actual storage is allocated fresh per invocation of `counter()`.

## 8. Common Pitfalls (continued)

1. **Creating closures inside hot loops** — every call to an outer function creates new function objects for its closures; doing this in a tight loop (e.g. attaching a new closure per array element per frame) can hurt performance.
2. **Forgetting closures are per-call, not per-function** — calling `counter()` twice produces two *independent* closures over two *independent* `count` variables.
3. **Debugging "stale" values** — if a closure appears to read an old value, check whether you intended to capture a snapshot (you didn't — closures are always live) versus a fresh variable per iteration (need `let`, not `var`).
