Kodokon kodokon.com

Closures y ámbito léxico

Domina cómo las closures capturan variables para encapsular estado y evitar las trampas del ámbito.

9 min · 3 preguntas

Abrir esta lección en Kodokon

Una closure no es un truco exótico: es el mecanismo por el cual una función conserva el acceso al ámbito léxico donde fue definida, no aquel donde se la llama. El motor no copia los valores: mantiene una referencia viva al entorno léxico. Dos llamadas a la misma fábrica producen por tanto dos entornos independientes, lo que convierte a las closures en la herramienta de encapsulación más ligera del lenguaje.

JAVASCRIPT
function createCounter() {
  let count = 0;
  return {
    increment: () => ++count,
    reset: () => { count = 0; },
  };
}
const counterA = createCounter();
const counterB = createCounter();
counterA.increment();
counterA.increment();
console.log(counterA.increment()); // 3
console.log(counterB.increment()); // 1
Estado privado: count solo es accesible a través de los métodos devueltos.

La trampa clásica del mundo real: var está ligada a la función que la envuelve, no al bloque. Dentro de un bucle, cada closure captura entonces la misma variable. Con let, cada iteración crea un entorno léxico nuevo, por lo que cada captura es distinta. Añade a eso la zona muerta temporal (TDZ): una variable let o const existe desde que se entra en el bloque, pero lanza un ReferenceError si la lees antes de su declaración.

JAVASCRIPT
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0); // 3, 3, 3
}
for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0); // 0, 1, 2
}
var comparte un único enlace; let crea uno por iteración.

En cuanto a patrones, las closures son la base de la memoización y del patrón módulo. Comparada con una clase, una closure te da privacidad genuina sin this ni prototipos, a costa de un uso de memoria ligeramente mayor cuando creas miles de instancias: cada instancia carga con sus propias funciones.

JAVASCRIPT
function memoize(fn) {
  const cache = new Map();
  return function (key) {
    if (cache.has(key)) return cache.get(key);
    const result = fn(key);
    cache.set(key, result);
    return result;
  };
}
const square = (n) => n * n;
const fastSquare = memoize(square);
console.log(fastSquare(9)); // computed
console.log(fastSquare(9)); // read from the cache
La caché vive dentro de la closure, invisible desde el exterior.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. En un bucle for (var i = ...), ¿qué imprimen tres callbacks de setTimeout creados dentro del cuerpo del bucle?
    • 0, 1, 2 porque cada callback captura su propio valor
    • 3, 3, 3 porque todos comparten el mismo enlace de i
    • undefined tres veces porque i se destruye tras el bucle
  2. ¿Qué conserva exactamente una closure del entorno donde fue definida?
    • Una copia congelada de los valores en el momento de la creación
    • Una referencia viva a las variables capturadas
    • Solo los parámetros de la función que la envuelve
    • Nada: el ámbito se destruye cuando la función retorna
  3. ¿Por qué createCounter garantiza que count permanezca privada?
    • Porque let hace la variable inmutable
    • Porque count solo es accesible a través de las funciones devueltas, las únicas que capturan su ámbito
    • Porque el motor elimina count después de que la fábrica retorna