Closures en JavaScript: desmitificando su uso paso a paso

Close-up of HTML code highlighted in vibrant colors on a computer monitor.
Foto de Pixabay en Pexels

Si alguna vez te has preguntado por qué una función sigue “recordando” variables aunque ya haya terminado su ejecución, la respuesta está en los closures. En este artículo vamos a desmontar el concepto, ver cómo se crea y, lo más importante, aplicarlo en ejemplos que realmente te pueden servir en día a día.

¿Qué es un closure?

Un closure es el conjunto de una función y el entorno léxico en el que se declaró. En otras palabras, la función mantiene una referencia a las variables que estaban a su alrededor cuando se creó, aun cuando esas variables ya no estén accesibles desde el resto del código.

Cómo se forma un closure

Todo lo que necesitas es una función interna (una inner function) que haga referencia a alguna variable de la función externa (outer function). Cuando la función externa termina, el motor de JavaScript no destruye esas variables porque la interna todavía las necesita.

Ejemplo clásico: contador privado

function crearContador(inicial = 0) {
  let cuenta = inicial; // variable del entorno léxico
  return function() {
    cuenta++;
    return cuenta;
  };
}

const contador = crearContador(5);
console.log(contador()); // 6
console.log(contador()); // 7
// La variable "cuenta" sigue viva dentro del closure

En este caso, cuenta nunca sale del alcance del closure, lo que nos da un estado privado que solo la función devuelta puede modificar.

Ejemplo práctico: debounce de eventos

Supongamos que quieres limitar la frecuencia con la que se ejecuta una función al escribir en un input. Un closure nos permite almacenar un timer entre llamadas sin exponerlo al resto del script.

function debounce(fn, espera = 300) {
  let timeoutId; // variable del closure
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), espera);
  };
}

const log = (msg) => console.log('Mensaje:', msg);
const logConDebounce = debounce(log, 500);

// En un listener de input
input.addEventListener('keyup', (e) => logConDebounce(e.target.value));

El timeoutId está encapsulado dentro del closure y no puede ser manipulado desde fuera, evitando colisiones con otros temporizadores.

Closures y módulos simples

Antes de que los módulos ES6 fueran estándar, los developers usaban closures para crear “espacios de nombres” y ocultar la implementación interna. Mira este patrón IIFE (Immediately Invoked Function Expression):

const miApi = (function() {
  const secret = 'clave-oculta';
  function publico() {
    console.log('Accediendo a', secret);
  }
  return { publico };
})();

miApi.publico(); // Accediendo a clave-oculta
// miApi.secret -> undefined

Aunque hoy usamos export/import, entender este enfoque ayuda a comprender por qué los closures son la base de la encapsulación en JavaScript.

Buenas prácticas al trabajar con closures

  • Evita crear closures dentro de bucles sin necesidad. Cada iteración generará una nueva función y, por tanto, un nuevo entorno léxico, lo que puede impactar en rendimiento.
  • No guardes referencias a objetos pesados dentro de closures si no los necesitas. El garbage collector no podrá liberar esa memoria mientras el closure exista.
  • Usa nombres descriptivos. Cuando la variable es parte del estado interno, un nombre como _cache o _timer indica claramente que es privada.

¿Cuándo usar un closure y cuándo no?

Los closures son perfectos para:

  • Crear funciones con configuración parcial (currying).
  • Encapsular datos que no deben exponerse públicamente.
  • Implementar patrones como debounce, throttle o memoization.

En cambio, si sólo necesitas una variable local y no vas a pasar la función a otro contexto, un simple bloque { } basta y evita la sobrecarga de crear un closure.

Conclusión

Los closures no son un truco de magia, sino una característica natural del modelo de alcance léxico de JavaScript. Entender cómo y por qué funcionan te permite escribir código más limpio, reutilizable y seguro. La próxima vez que necesites un estado privado, un temporizador o una función configurada, piensa en los closures antes de buscar una solución externa.