Entendiendo los Closures en JavaScript: más allá del alcance

Close-up view of HTML code displayed on a dark screen, showcasing coding concepts.
Foto de Florian Holly en Pexels

Los closures son una de esas características de JavaScript que suenan misteriosas hasta que las vemos en acción. No se trata solo de "una función que recuerda su entorno", sino de cómo el motor de JavaScript gestiona la memoria y el scope chain para que esa información siga disponible cuando la necesitemos.

¿Qué ocurre bajo el capó?

Cuando declaras una función dentro de otra, JavaScript crea un lexical environment para cada una. Cada entorno contiene:

  • Una referencia al scope externo (el outer lexical environment).
  • Un record con las variables locales.

Si la función interna se devuelve o se pasa como callback, el motor no la elimina al salir del bloque exterior. En su lugar, mantiene una referencia al entorno externo. Esa referencia es lo que llamamos closure.

Ejemplo clásico: contador privado

function crearContador(inicial = 0) {
  let cuenta = inicial; // variable del entorno externo
  return {
    incrementar() {
      cuenta++;
      return cuenta;
    },
    obtener() {
      return cuenta;
    }
  };
}

const contador = crearContador(5);
console.log(contador.incrementar()); // 6
console.log(contador.obtener());     // 6

Aquí cuenta nunca sale del scope de crearContador, pero los métodos devueltos siguen accediendo a ella gracias al closure.

Closures y el heap vs la pila

En la mayoría de los motores, las variables locales se almacenan en la pila mientras la función está en ejecución. Cuando una función crea un closure, esas variables se “promocionan” al heap, porque su vida útil supera al marco de pila que las creó. Por eso es importante no crear referencias innecesarias a objetos pesados dentro de un closure, ya que impedirán que el recolector de basura los libere.

Patrón módulo con IIFE

Antes de que los módulos ES6 existieran, los closures eran la base para crear módulos encapsulados mediante Immediately Invoked Function Expressions (IIFE). Veamos una variante moderna usando const y un IIFE:

const api = (() => {
  const token = 'secreto-123'; // privado
  const fetchDatos = async (url) => {
    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${token}` }
    });
    return res.json();
  };
  return { fetchDatos };
})();

// Uso
api.fetchDatos('/api/usuarios').then(console.log);

El token nunca sale del módulo, pero todas las funciones internas pueden usarlo sin exponerlo al exterior.

Currying con closures

Otra aplicación práctica es el currying: transformar una función que recibe varios argumentos en una cadena de funciones que reciben uno a la vez. Cada paso crea un closure que guarda los argumentos parciales.

const suma = a => b => c => a + b + c;
const suma5 = suma(5);
const suma5y3 = suma5(3);
console.log(suma5y3(2)); // 10

En el ejemplo, a y b quedan “capturados” en closures sucesivos, permitiendo construir funciones más específicas a medida que se suministran los argumentos.

Closures y asincronía

Los callbacks asíncronos, como los de setTimeout o promesas, son un escenario donde los closures brillan. Cada vez que la función asíncrona se ejecuta, sigue teniendo acceso a las variables del contexto donde se creó.

function crearTemporizador(mensaje, retraso) {
  setTimeout(() => {
    console.log(`${mensaje} (después de ${retraso}ms)`);
  }, retraso);
}

crearTemporizador('Hola mundo', 1500);

El mensaje y retraso están disponibles cuando el setTimeout se dispara, aunque la función original haya terminado hace mucho tiempo.

Buenas prácticas

  • Evita capturar objetos grandes si solo necesitas una pequeña propiedad.
  • Desasigna referencias cuando ya no las necesites (p. ej., handler = null) para ayudar al GC.
  • Usa funciones puras cuando sea posible; los closures pueden introducir estado implícito que dificulta el testing.
  • Documenta el cierre en la firma JSDoc para que otros desarrolladores sepan qué variables están siendo capturadas.

Dominar los closures te permite escribir código más modular, reutilizable y seguro. Desde crear APIs privadas hasta construir funciones curried o manejar lógica asíncrona, los closures son una herramienta esencial en el día a día de cualquier desarrollador frontend.