Closures en JavaScript: conceptos clave y casos de uso cotidianos

Si alguna vez te has topado con la frase “una función que recuerda su entorno” y no has sabido qué significa, este artículo es para ti. Los closures son uno de esos conceptos de JavaScript que suenan misteriosos, pero que en la práctica aparecen en cada proyecto, desde una simple utilidad hasta una arquitectura completa.
¿Qué es un closure?
En JavaScript, una función forma un closure cuando conserva una referencia a las variables que estaban en su ámbito léxico en el momento de su creación, aun cuando ese ámbito haya finalizado. En otras palabras, la función “cierra” sobre esas variables y las lleva consigo.
Cómo se crea un closure
Todo comienza con una función interna (a veces llamada función anidada) que se devuelve o se pasa como callback. Cuando el motor de JavaScript crea esa función interna, guarda una [[Environment]] que apunta al contexto de ejecución donde estaba definida. Esa referencia nunca se elimina mientras exista alguna referencia a la función interna.
function crearSaludo(saludo) {
return function(nombre) {
console.log(`${saludo}, ${nombre}!`);
};
}
const hola = crearSaludo('¡Hola');
const adios = crearSaludo('Adiós');
hola('Álvaro'); // → ¡Hola, Álvaro!
adios('María'); // → Adiós, María!
En el ejemplo anterior, la función anónima que devuelve crearSaludo mantiene viva la variable saludo aun después de que crearSaludo haya terminado su ejecución.
Ejemplo real: un debouncer
Un caso de uso cotidiano es el debouncer, una técnica que limita la frecuencia con la que se ejecuta una función. Es muy útil en eventos como keyup o scroll, donde queremos evitar llamadas excesivas a una API.
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
// Uso en un input de búsqueda
const buscar = query => console.log('Buscando:', query);
const buscarConDelay = debounce(buscar, 300);
// Simulamos varios eventos rápidos
['a','ab','abc'].forEach((val, i) => {
setTimeout(() => buscarConDelay(val), i * 100);
});
Aquí, debounce devuelve una nueva función que, gracias al closure, mantiene la variable timeoutId entre invocaciones. Cada vez que el usuario escribe, el temporizador anterior se cancela y se programa uno nuevo, garantizando que buscar sólo se ejecute cuando el usuario haya dejado de teclear al menos 300 ms.
Ventajas y trampas
- Encapsulamiento: Los closures permiten crear datos privados sin recurrir a clases o módulos externos.
- Memoria: Cada closure conserva su propio entorno, lo que puede generar consumo de memoria si se crean cientos de ellos sin necesidad.
- Referencias circulares: Si un closure mantiene una referencia a un objeto que a su vez referencia al closure, el recolector de basura no podrá liberar la memoria.
- Depuración: En la consola, a veces resulta confuso ver por qué una variable tiene un valor inesperado; recordar que está “capturada” por un closure ayuda a entender el comportamiento.
Para evitar sorpresas, es buena práctica:
- Limitar la cantidad de datos que captura un closure.
- Eliminar referencias a funciones internas cuando ya no sean necesarias.
- Usar
WeakMapoWeakSetsi necesitas asociar datos a objetos sin impedir su recolección.
Conclusión
Los closures son el pegamento que hace que JavaScript sea tan flexible: permiten crear funciones con estado, construir APIs limpias y controlar la ejecución asíncrona de forma elegante. La clave está en entender que la función interna lleva consigo su entorno léxico, y que ese “viaje en el tiempo” es lo que nos brinda poder –y, a veces, problemas de rendimiento.
La próxima vez que necesites un contador, una caché ligera o una lógica de throttling, piensa en los closures antes de buscar una solución externa. Con práctica, pasarás de verlos como un concepto abstracto a utilizarlos como una herramienta cotidiana en tu caja de trucos de frontend.