Closures en JavaScript explicados con ejemplos reales

Close-up of a colorful code snippet on a computer screen, highlighting programming concepts.
Foto de Drishan Dey en Pexels

En JavaScript, un closure es una función que recuerda el entorno en el que se creó, aunque se ejecute fuera de él. Este concepto es fundamental para escribir código modular, mantener datos privados y crear funciones reutilizables.

¿Qué ocurre bajo el capó?

Cuando declaras una función dentro de otra, la interna conserva una referencia al scope de la externa. Esa referencia incluye variables, parámetros y cualquier otro recurso disponible en el momento de la creación. Así, aunque la función externa haya terminado su ejecución, la interna sigue teniendo acceso a esas variables.

Ejemplo clásico: un contador privado

Imagina que necesitas varios contadores independientes sin exponer su estado interno. Con un closure, puedes encapsular la variable del contador y ofrecer sólo los métodos necesarios.

function crearContador(inicial) {
  let valor = inicial || 0;
  return {
    incrementar: function() {
      valor++;
      return valor;
    },
    decrementar: function() {
      valor--;
      return valor;
    },
    obtener: function() {
      return valor;
    }
  };
}

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

const contadorB = crearContador();
console.log(contadorB.decrementar()); // -1

En este caso, valor nunca está accesible desde fuera del objeto devuelto, lo que garantiza la privacidad del estado.

Closures y callbacks asíncronos

Los closures brillan cuando trabajamos con operaciones asíncronas, como setTimeout o peticiones fetch. Cada vez que se programa una función que se ejecutará más tarde, esa función conserva el contexto en el que se creó.

function saludarDespuesDe(segundos, nombre) {
  setTimeout(function() {
    console.log(Hola, ${nombre}!);
  }, segundos * 1000);
}

saludarDespuesDe(2, 'Álvaro'); // Después de 2 s: "Hola, Álvaro!"

Aunque la llamada a saludarDespuesDe haya terminado, la función anónima dentro de setTimeout sigue recordando el valor de nombre. Este patrón es muy útil para mantener datos de contexto en peticiones AJAX, animaciones o cualquier lógica diferida.

Uso práctico en módulos y factories

Los closures también son la base de los factory functions y de los módulos reveladores (revealing module pattern). Permiten crear APIs limpias sin exponer variables internas.

  • Factory functions: crean objetos con estado privado.
  • Módulo revelador: devuelve sólo los métodos que queremos que sean públicos.

Ejemplo de módulo revelador que gestiona una lista de tareas:

const gestorTareas = (function() {
  const tareas = [];

  function agregar(tarea) {
    tareas.push(tarea);
  }

  function listar() {
    return [...tareas]; // copia para evitar mutaciones externas
  }

  function eliminar(indice) {
    tareas.splice(indice, 1);
  }

  return { agregar, listar, eliminar };
})();

gestorTareas.agregar('Aprender closures');
console.log(gestorTareas.listar()); // ['Aprender closures']

El array tareas nunca es accesible directamente desde fuera del IIFE; sólo podemos interactuar a través de los métodos expuestos.

Trampas habituales y buenas prácticas

Aunque los closures son poderosos, pueden generar fugas de memoria si retenemos referencias a objetos muy pesados sin necesidad. Algunas recomendaciones:

  • Evita crear closures dentro de bucles si no los necesitas; reutiliza funciones definidas fuera del bucle.
  • Cuando trabajes con eventos, asegúrate de eliminar los listeners si el componente se desmonta.
  • Prefiere const y let en lugar de var para evitar confusiones con el ámbito de función.

En resumen, los closures son una herramienta esencial para cualquier desarrollador front‑end. Te permiten encapsular lógica, mantener datos privados y crear APIs limpias. Dominar su uso te ayudará a escribir código más robusto y fácil de mantener.