Closures en JavaScript: cómo usarlos en proyectos reales

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

En el día a día de un desarrollador frontend los closures aparecen una y otra vez, aunque no siempre los reconozcamos. En este artículo vamos a desmitificar el concepto, ver cómo se crean y, lo más importante, aplicarlos a problemas reales que puedes encontrarte en tus proyectos.

¿Qué es un closure?

Un closure es simplemente una función que recuerda el entorno léxico en el que se definió, incluso después de que la función externa haya terminado su ejecución. Esa “memoria” incluye variables, parámetros y otras funciones accesibles en el scope donde se creó.

Cómo se forma un closure

Para que se genere un closure necesitas dos cosas:

  • Una función interna (la que será devuelta o asignada a una variable).
  • Un scope externo que contenga variables que la interna quiera usar.

Cuando la función interna se devuelve, JavaScript crea un objeto interno que enlaza esa función con su scope original. Ese objeto es el closure.

Ejemplo clásico: contador privado

function crearContador(inicial = 0) {
  let cuenta = inicial;
  return {
    incrementar() {
      cuenta++;
      return cuenta;
    },
    obtener() {
      return cuenta;
    }
  };
}

const contador = crearContador(5);
console.log(contador.incrementar()); // 6
console.log(contador.obtener()); // 6
// La variable "cuenta" no es accesible desde fuera del closure

En este caso cuenta está encapsulada dentro del closure creado por crearContador. Ningún código externo puede modificarla directamente, lo que evita errores inesperados.

Aplicación real: módulo de API

Imagina que tienes que interactuar con una API y quieres centralizar la lógica de token y cabeceras. Un closure nos permite guardar el token una sola vez y reutilizarlo sin exponerlo globalmente.

function apiClient(baseUrl) {
  let token = null;

  function setToken(t) { token = t; }
  async function get(endpoint) {
    const res = await fetch(`${baseUrl}${endpoint}`, {
      headers: { Authorization: `Bearer ${token}` }
    });
    return res.json();
  }

  return { setToken, get };
}

const client = apiClient('https://api.ejemplo.com');
client.setToken('mi-token-secreto');
client.get('/users').then(console.log);

El token está protegido dentro del closure del apiClient, evitando que cualquier parte del código lo sobrescriba por accidente.

Debounce con closure: evitar peticiones excesivas

Un patrón muy usado en interfaces es el debounce, que retrasa la ejecución de una función hasta que pasa un intervalo sin nuevas llamadas. Aquí el closure guarda el timerId entre invocaciones.

function debounce(fn, wait) {
  let timeoutId;
  return function(...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), wait);
  };
}

const buscar = debounce((q) => console.log('Buscando:', q), 300);
// Simulamos escritura rápida
buscar('a'); buscar('ab'); buscar('abc'); // Sólo se ejecuta una vez tras 300 ms

El timeoutId persiste entre llamadas gracias al closure, permitiendo cancelar la ejecución previa.

Buenas prácticas y trampas comunes

  • Evita referencias a objetos grandes: si el closure captura un objeto pesado, ese objeto permanecerá en memoria mientras el closure exista.
  • No abuses de los closures para “ocultar” datos: la encapsulación no es seguridad; cualquier código que tenga acceso al closure puede leer o modificar esas variables.
  • Prefiere funciones flecha sólo cuando no necesites this dinámico: las flechas heredan this del scope externo, lo que a veces genera confusión.

Conclusión

Los closures son una herramienta esencial en JavaScript que nos permite crear funciones con estado propio, encapsular lógica y escribir código más modular. Desde contadores privados hasta clientes de API y utilidades como debounce, los ejemplos mostrados demuestran cómo aplicar este concepto a problemas cotidianos. La clave está en entender que el closure mantiene una referencia al scope original, lo que abre un abanico de posibilidades pero también implica responsabilidad a la hora de gestionar la memoria.