Closures en JavaScript: cómo crear APIs privadas y memorizar resultados

Close-up of colorful programming code displayed on a computer screen, showcasing modern coding concepts.
Foto de Markus Spiske en Pexels

Los closures son uno de esos conceptos de JavaScript que suenan complicados pero que, con el ejemplo correcto, se vuelven una herramienta cotidiana. En este artículo veremos dos usos que rara vez aparecen juntos: construir APIs privadas que encapsulan estado y aplicar memorization (caché) a funciones costosas.

¿Qué es un closure?

En palabras simples, un closure es la combinación del entorno léxico de una función y la propia función. Cuando una función se declara dentro de otra, mantiene una referencia a las variables de la función externa, incluso después de que esta haya terminado su ejecución.

1️⃣ API privada con closure

Imagina que quieres exponer un contador, pero que sólo pueda modificarse a través de métodos controlados. En vez de usar una clase o variables globales, podemos usar un closure para encapsular el estado.

function crearContador(inicial = 0) {
  let valor = inicial; // variable privada
  return {
    incrementar() {
      valor += 1;
      return valor;
    },
    decrementar() {
      valor -= 1;
      return valor;
    },
    obtener() {
      return valor;
    }
  };
}

const contador = crearContador(5);
console.log(contador.obtener());   // 5
console.log(contador.incrementar()); // 6
console.log(contador.decrementar()); // 5
// console.log(contador.valor); // undefined → está privado

En el ejemplo, valor nunca sale del scope de crearContador. Sólo los métodos que devolvemos pueden interactuar con él, creando una API limpia y segura.

2️⃣ Memorization con closure

Otro caso típico es evitar cálculos repetitivos. Supongamos que tenemos una función recursiva para calcular el factorial, que puede llamarse muchas veces con los mismos argumentos.

function memoizar(fn) {
  const cache = new Map();
  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 factorial = memoizar(function fac(n) {
  return n <= 1 ? 1 : n * fac(n - 1);
});

console.log(factorial(5)); // 120 (cálculo)
console.log(factorial(5)); // 120 (desde caché)

La función memoizar crea un Map que persiste entre llamadas gracias al closure. Cada vez que la función envuelta se invoca, primero revisa el caché y devuelve el valor almacenado si ya existe.

⚠️ Cuidado con los leaks

  • Referencias innecesarias: si guardas objetos muy grandes en el caché sin una estrategia de expiración, la memoria del navegador crecerá.
  • Contexto de this: al usar fn.apply(this, args) preservamos el contexto, pero si el closure captura this de forma inesperada puedes crear referencias circulares.
  • Claves de caché: usar JSON.stringify funciona para argumentos simples, pero no es fiable con funciones o objetos con orden de propiedades variable.

Mejores prácticas

  • Limita el tamaño del caché; por ejemplo, usando un LRU cache o borrando entradas después de un tiempo.
  • Documenta qué variables quedan “privadas” en tu closure para que otros desarrolladores comprendan el contrato.
  • Prefiere funciones puras para memoizar; si la función tiene efectos secundarios, el caché puede producir resultados inesperados.
  • En entornos con hot‑module‑replacement (HMR) o pruebas, asegúrate de reinicializar los closures para evitar estado compartido entre pruebas.

Con estos dos patrones –API privada y memorization– puedes aprovechar al máximo los closures, manteniendo tu código modular, seguro y eficiente. La próxima vez que necesites ocultar estado o optimizar cálculos, recuerda que un simple function dentro de otra ya te está ofreciendo todo lo necesario.