Chapter 19

Closures: The Precise Definition of Function + Lexical Environment

A closure = a function object + the lexical environment record captured at creation time; it is the foundation of private state and modularity in JavaScript

The ECMAScript specification never uses the word "closure"โ€”the spec describes the [[Environment]] internal slot of function objects, which points to the lexical Environment Record (ER) that existed when the function was created. "Closure" is the colloquial name for this mechanism: a function is packaged together with the lexical environment from its point of definition. Even after the outer function has finished executing and its Execution Context has been popped from the stack, that Environment Record is still held by the function object and is not garbage-collected.

The practical value of closures is precise: they allow a function to access variables from the scope in which it was defined, regardless of when and where that function is eventually called. This is the foundational mechanism behind factory functions, the module pattern, event handlers that preserve state, memoization, currying, and almost every other higher-order programming pattern in JavaScript. Closures are also the most common hidden source of memory leaksโ€”understanding their internal mechanics is a prerequisite for writing high-performance code.


๐Ÿ”น Level 1 ยท What You Need to Know

The Core Behavior of a Closure

function makeCounter() {
  let count = 0; // This variable survives after makeCounter finishes

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

const counter = makeCounter(); // makeCounter finishes; count is still alive
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

After makeCounter finishes, its Execution Context (EC) is popped from the call stack, but the Environment Record (ER) containing count is not destroyedโ€”because the increment function object holds a reference to this ER via its [[Environment]] internal slot. As long as the counter variable is alive, this ER will not be garbage-collected.

Factory Functions: Creating Independent Private State with Closures

function makeCounter(initialValue = 0) {
  let count = initialValue;

  return {
    increment() { count++; },
    decrement() { count--; },
    value()     { return count; }
  };
}

const c1 = makeCounter(0);
const c2 = makeCounter(100);

c1.increment();
c1.increment();
console.log(c1.value()); // 2
console.log(c2.value()); // 100 (c2 has its own independent count, unaffected by c1)

c1 and c2 each hold the distinct lexical environment produced by their respective makeCounter calls, so count is completely independent. This was the core pattern for implementing "private variables" in JavaScript before ES Modules became widespread.

Common Closure Use Cases

Event handlers that preserve state:

function setupButton(label) {
  let clickCount = 0;

  document.getElementById('btn').addEventListener('click', function() {
    clickCount++; // accesses the outer clickCount
    console.log(`${label} has been clicked ${clickCount} times`);
  });
}

setupButton('Submit'); // each click accesses the latest clickCount

Memoize (caching function results):

function memoize(fn) {
  const cache = new Map(); // cache is shared across all memoized calls

  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) {
      return cache.get(key);
    }
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveCalc = memoize(function(n) {
  // simulate an expensive computation
  return n * n;
});

console.log(expensiveCalc(10)); // 100 (computed)
console.log(expensiveCalc(10)); // 100 (returned from cache, no recomputation)

The Connection Between Closures and Memory Leaks

A closure holds the entire lexical Environment Record, even if it only uses one variable from it:

function createHandler() {
  const bigData = new Array(1000000).fill('data'); // ~1 MB of data
  const smallValue = 42;

  // This closure only uses smallValue, but it holds the entire ER
  // bigData therefore cannot be GC'd
  return function handler() {
    return smallValue;
  };
}

const handler = createHandler();
// As long as handler is alive, bigData will not be released

๐Ÿ”ธ Level 2 ยท How It Actually Works

The [[Environment]] Internal Slot of Function Objects

Every function object created via the function keyword, an arrow function, or a method definition has an [[Environment]] internal slot. This slot stores the lexical Environment Record that was active when the function was created (defined).

When a function is created:
  The LexicalEnvironment of the currently running EC โ†’ ER_outer
  โ†“
  New function object fn
  fn.[[Environment]] = ER_outer   โ† captures the lexical environment at creation time

When the function is called, the engine executes NewFunctionEnvironment to create a new FunctionEnvironmentRecord:

When fn() is called:
  Create a new FunctionEnvironmentRecord (ER_fn)
  ER_fn.[[OuterEnv]] = fn.[[Environment]]   โ† connects to the outer ER captured by the closure
  Create a new EC, set LexicalEnvironment = ER_fn
  Push EC onto the EC Stack

This is the physical mechanism by which the scope chain forms a closure: the outer ER of the new EC is the ER from when the function was defined, not the ER at the call site.

Memory Structure Diagram of a Closure

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚                  Memory Structure of a Closure                   โ”‚
โ”‚                                                                 โ”‚
โ”‚  Heap                                                           โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
โ”‚  โ”‚  makeCounter's ER (lexical Environment Record)           โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  bindings:                                          โ”‚ โ”‚   โ”‚
โ”‚  โ”‚  โ”‚    count โ†’ 3                                        โ”‚ โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  [[OuterEnv]] โ†’ Global ER                           โ”‚ โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚   โ”‚
โ”‚  โ”‚           โ†‘                                              โ”‚   โ”‚
โ”‚  โ”‚           โ”‚ [[Environment]] internal slot reference      โ”‚   โ”‚
โ”‚  โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  increment function object                         โ”‚  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  [[Environment]] โ†’ makeCounter's ER โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ”‚  โ”‚  [[Call]]        โ†’ function code                      โ”‚   โ”‚
โ”‚  โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  โ”‚   โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
โ”‚                                                                 โ”‚
โ”‚  Stack variable:                                                โ”‚
โ”‚  counter โ†’ points to the increment function object             โ”‚
โ”‚                                                                 โ”‚
โ”‚  GC reachability:                                               โ”‚
โ”‚  counter (stack var) โ†’ increment function obj โ†’ makeCounter ER  โ”‚
โ”‚  โ†’ count variable (value 3)                                     โ”‚
โ”‚  As long as counter is alive, the entire chain will not be GC'd โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

The Classic Closure-in-Loop Problem: var vs. let

var version: all closures share the same i in the same ER

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(function() { return i; });
}

// i = 3 now (i persists in the function scope after the loop)
console.log(funcs[0]()); // 3
console.log(funcs[1]()); // 3
console.log(funcs[2]()); // 3
var i's scope: function (or global) ER
  i: 3 (value after loop ends)
  โ†‘
  funcs[0].[[Environment]] โ”€โ”
  funcs[1].[[Environment]] โ”€โ”ค all point to the SAME ER!
  funcs[2].[[Environment]] โ”€โ”˜

let version: each iteration creates a new ER; each closure has an independent i

const funcs = [];
for (let i = 0; i < 3; i++) {
  funcs.push(function() { return i; });
}

console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2
iter 0: ERโ‚€ { i: 0 } โ† funcs[0].[[Environment]]
iter 1: ERโ‚ { i: 1 } โ† funcs[1].[[Environment]]
iter 2: ERโ‚‚ { i: 2 } โ† funcs[2].[[Environment]]
(three independent ERs, three independent values of i)

V8's Closure Optimization: Only Retaining Actually-Referenced Variables

V8 (Chrome/Node.js) performs an important optimization: through static analysis, only variables that are actually referenced by inner functions are retained in the closure's captured ER; unreferenced variables are not kept in the ER.

function outer() {
  const bigData = new Array(1000000).fill(0); // ~4 MB
  const smallValue = 42;

  // Only smallValue is referenced; V8's ER only retains smallValue
  // bigData can be GC'd (not held by any live reference)
  return function inner() {
    return smallValue;
  };
}

const fn = outer();
// In V8: bigData has been GC'd; only smallValue is kept alive

However, this optimization has two important exceptions that prevent V8 from optimizing:

  1. The presence of eval: if a closure contains eval, V8 cannot statically determine which variables may be accessed (eval can access any variable name), so the entire ER is retained
  2. The with statement: same effectโ€”prevents static analysis; V8 retains the complete ER
function withEval() {
  const bigData = new Array(1000000).fill(0); // ~4 MB
  const smallValue = 42;

  return function inner(code) {
    eval(code); // the mere presence of eval causes V8 to retain the entire ER; bigData cannot be GC'd
    return smallValue;
  };
}

React Hooks Stale Closure Problem

A stale closure is one that has captured an old version of a variable rather than the latest value:

// React component (demonstrating stale closure)
function Counter() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    // This effect runs on mount and captures count at that time (value: 0)
    const timer = setInterval(() => {
      // Runs every second, but count is always 0 (stale closure)
      console.log('current count:', count); // always 0
    }, 1000);

    return () => clearInterval(timer);
  }, []); // Empty dependency array: effect runs only on mount, never re-runs

  return <button onClick={() => setCount(c => c + 1)}>Click: {count}</button>;
}

Correct approach 1: Add count to the dependency array (effect re-runs every time count changes):

React.useEffect(() => {
  const timer = setInterval(() => {
    console.log('current count:', count); // correct: re-captured each time count changes
  }, 1000);
  return () => clearInterval(timer);
}, [count]); // depends on count

Correct approach 2: Use useRef to hold the latest value (ref changes do not trigger re-renders):

const countRef = React.useRef(count);
countRef.current = count; // update ref on every render

React.useEffect(() => {
  const timer = setInterval(() => {
    console.log('current count:', countRef.current); // always the latest value
  }, 1000);
  return () => clearInterval(timer);
}, []); // ref does not need to be in the dependency array

๐Ÿ”บ Level 3 ยท What the Specification Says

Specification Section 10.2.1: [[Environment]] Internal Slot

Section 10.2.1 (ECMAScript Function Objects) defines the internal slots of function objects:

Internal Slot Type Description
[[Environment]] Environment Record The lexical environment where the function was defined
[[PrivateEnvironment]] PrivateEnvironment Record The environment for class private fields
[[FormalParameters]] Parse Node The parse node for the parameter list
[[ECMAScriptCode]] Parse Node The parse node for the function body
[[Realm]] Realm Record The Realm to which the function belongs
[[HomeObject]] Object / undefined The object to which the method belongs (for super)
[[IsClassConstructor]] Boolean Whether it is a class constructor

OrdinaryFunctionCreate is the specification operation for creating an ordinary function object (simplified):

OrdinaryFunctionCreate(proto, sourceText, parameterList, body, thisMode, env, privateEnv):
1. Create a new function object F
2. F.[[Environment]] = env   โ† env is the LexicalEnvironment of the current running EC
3. F.[[FormalParameters]] = parameterList
4. F.[[ECMAScriptCode]] = body
5. F.[[ThisMode]] = thisMode  (lexical/strict/global; lexical for arrow functions)
6. F.[[Realm]] = current Realm
7. ... other initialization ...
8. Return F

Note step 2: the env argument comes from the LexicalEnvironment of the currently running EC when OrdinaryFunctionCreate is called. This is the specification basis for "a function captures the lexical environment of wherever it is defined."

NewFunctionEnvironment: Creating a New ER at Call Time

The specification's NewFunctionEnvironment(F, newTarget) operation (Section 9.1.2.4):

NewFunctionEnvironment(F, newTarget):
1. Create a new FunctionEnvironmentRecord env
2. env.[[FunctionObject]] = F
3. If F.[[ThisMode]] is lexical โ†’ env.[[ThisBindingStatus]] = 'lexical'
   Otherwise โ†’ env.[[ThisBindingStatus]] = 'uninitialized'
4. env.[[NewTarget]] = newTarget
5. env.[[OuterEnv]] = F.[[Environment]]   โ† connects to the outer ER captured by the closure!
6. Return env

Step 5 is the key: the new FunctionEnvironmentRecord's [[OuterEnv]] points to F.[[Environment]] (the ER from when the function was defined), not the ER at the current call site. This is the physical implementation of lexical scoping at function call time.

Closure Binding in FunctionDeclarationInstantiation

Specification Section 10.2.1.1 (FunctionDeclarationInstantiation) performs initialization before the function body executes. In the steps that handle inner function declarations, the engine calls InstantiateOrdinaryFunctionObject for each inner function, which internally calls OrdinaryFunctionCreate with the current EC's env as the [[Environment]] argumentโ€”this is the specification-level description of how an inner function "captures" the outer function's environment.

import bindings in modules (a specialized form of closure): Bindings created by import in a ModuleEnvironmentRecord are "indirect bindings"โ€”they do not store a value directly; instead they store "a reference to a particular name in some other ER." When an import binding is read, the spec requires reading through to the ER where the value is actually stored. This gives ESM exports and imports a "live connection"โ€”if the exporting module changes a value, the importing side immediately sees the new value. (This is not a closure per se, but the mechanism resembles closure-style reference passing rather than value copying.)


๐Ÿ’Ž Level 4 ยท Edge Cases and Traps

Trap 1: A Closure Holds the Entire ER, Not Just the Variables It Uses (With V8 Optimization Exceptions)

function createLeak() {
  const hugeArray = new Float64Array(10 * 1024 * 1024); // 80 MB
  let counter = 0;

  // These closures only use counter, not hugeArray
  // But without V8 optimization (if eval or with is present), hugeArray cannot be GC'd
  return {
    increment() { counter++; },
    value() { return counter; }
  };
}

const obj = createLeak();
// obj holds two closures; each points to createLeak's ER
// The ER contains hugeArray (80 MB); hugeArray cannot be GC'd

Diagnostic tool: Chrome DevTools โ†’ Memory โ†’ Take Heap Snapshot. Search for hugeArray in the snapshot or look for objects of type "Closure" to see which variables are being held alive by closures.

Solution: explicitly set large objects to null when they are no longer needed:

function createSafe() {
  let hugeArray = new Float64Array(10 * 1024 * 1024);
  let counter = 0;

  // Explicitly release the large data after use
  const result = processData(hugeArray);
  hugeArray = null; // break the reference; allow GC

  return {
    increment() { counter++; },
    value() { return counter; },
    result() { return result; }
  };
}

Trap 2: eval and with Block V8 Closure Optimization, Causing Unexpected Memory Leaks

// Without eval: V8 can optimize; bigData is GC'd
function withoutEval() {
  const bigData = new Uint8Array(50 * 1024 * 1024); // 50 MB
  const value = 1;
  return () => value;
}

// With eval: V8 does not optimize; bigData cannot be GC'd
function withEval() {
  const bigData = new Uint8Array(50 * 1024 * 1024); // 50 MB
  const value = 1;
  return (code) => {
    eval(code);  // even if eval is never executed, its presence blocks the optimization
    return value;
  };
}

const fn1 = withoutEval(); // bigData is GC'd; ~0 MB sustained usage
const fn2 = withEval();    // bigData cannot be GC'd; 50 MB sustained usage

// Measuring the memory impact
const mem = performance.memory; // provided by Chrome
// While fn2 is alive, heapUsed will be ~50 MB higher

Engineering practice: unless there is a clear and deliberate need for dynamic code execution, never use eval or new Function(string) (new Function is subject to the same restrictions) in production code.

Trap 3: Adding Event Listeners Repeatedly in a Loop Creates Multiple Closures Holding DOM References

// Wrong: each call to setup adds a new listener without removing the old one
function setup() {
  const button = document.getElementById('btn');
  const heavyData = fetchHeavyData(); // large object

  // Each setup() call adds a new closure holding heavyData
  button.addEventListener('click', function handler() {
    process(heavyData); // closure holds heavyData
  });
}

// If setup is called multiple times (e.g., on component re-mount),
// handlers accumulate: each holds a reference to heavyData โ†’ memory grows continuously
setup(); setup(); setup(); // 3 closures, 3 copies of heavyData

Correct approach: remove the old listener before adding a new one, or use { once: true }:

let currentHandler = null;

function setup() {
  const button = document.getElementById('btn');
  const heavyData = fetchHeavyData();

  // Remove the old handler first
  if (currentHandler) {
    button.removeEventListener('click', currentHandler);
  }

  currentHandler = function handler() {
    process(heavyData);
  };

  button.addEventListener('click', currentHandler);
}

Trap 4: How IIFE Solves the var Loop Problem

Before let became standard, the canonical fix for the closure-in-loop problem was the IIFE (Immediately Invoked Function Expression):

const funcs = [];
for (var i = 0; i < 3; i++) {
  funcs.push(
    (function(capturedI) {  // IIFE creates a new function scope
      return function() { return capturedI; };
    })(i)  // pass the current value of i as an argument
  );
}

console.log(funcs[0]()); // 0
console.log(funcs[1]()); // 1
console.log(funcs[2]()); // 2

How IIFE works: on each iteration, (function(capturedI) {...})(i) is immediately invoked, creating a new FunctionEnvironmentRecord with its own capturedI binding (valued at the current i). The inner function function() { return capturedI; } has its [[Environment]] pointing to this IIFE's ER, not to the outer ER where var i lives. At the specification level, this achieves the same effect as let creating a new ER for each iteration.

In modern code, let is the cleaner solutionโ€”the two are semantically equivalent, but let does not incur the overhead of an additional function call.

Trap 5: The Module Pattern (IIFE Returning an Object) Is a Classic Closure Application

// Module pattern: the standard way to achieve private variables and a public interface before ESM
const UserModule = (function() {
  // Private variables (not directly accessible from outside)
  let users = [];
  let nextId = 1;

  // Private function
  function validate(user) {
    return user.name && user.name.length > 0;
  }

  // Public interface
  return {
    add(name) {
      const user = { id: nextId++, name };
      if (!validate(user)) throw new Error('Invalid username');
      users.push(user);
      return user.id;
    },

    get(id) {
      return users.find(u => u.id === id);
    },

    count() {
      return users.length;
    }
  };
})();

UserModule.add('Alice'); // 1
UserModule.add('Bob');   // 2
console.log(UserModule.count()); // 2
console.log(UserModule.users);   // undefined (private; not accessible)

The essence of the module pattern: the IIFE executes immediately, creating a FunctionEnvironmentRecord that holds users, nextId, and validate. Each method on the returned object is a closure; all their [[Environment]] slots point to this same ER. External code can only interact through the returned public interface and cannot directly read or write users or nextId, achieving encapsulation.

ESM's export/import provides better modularity semantically (static analysis, tree-shaking support, no IIFE needed), but when learning closures, the module pattern is the most intuitive comprehensive example.


Summary

  1. A closure is a function object paired with the lexical Environment Record captured at its creation time: every function object has an [[Environment]] internal slot pointing to the ER active when it was defined; when the function is called, the new FunctionEnvironmentRecord's [[OuterEnv]] points to [[Environment]], forming a scope chain that spans across call stack frames.

  2. The difference between var and let in loops: all iteration closures under var share the same variable in a single ER; let uses CreatePerIterationEnvironment to create an independent ER for each iteration, giving each closure its own independent variable value.

  3. A closure holds the entire lexical Environment Record, even if it references only one of its variables. V8 optimizes closures through static analysis (retaining only actually-referenced variables), but the presence of eval or with blocks this optimization, causing the entire ER (and all its variables) to be retained, leading to unexpected memory consumption.

  4. The stale closure problem in React Hooks arises because a useEffect callback captures the state value at the time of its creation; if that state is not in the dependency array, the closure continues to hold the old value even after subsequent state updates. Solutions include adding the state to the dependency array or using useRef to hold the latest value.

  5. To proactively release large objects held by closures: explicitly set variables to null when they are no longer needed to break the ER's reference to the large object; always call removeEventListener when listeners are no longer needed to prevent closures from holding DOM nodes and large objects alive indefinitely.

Rate this chapter
4.6  / 5  (11 ratings)

๐Ÿ’ฌ Comments