Una closure es una función que recuerda el entorno en el que fue creada, incluyendo todas las variables del scope que la rodea, aunque ese scope ya haya terminado de ejecutarse. No es una característica exótica de JavaScript: es el mecanismo que hace posibles los callbacks, los módulos, la memoización y buena parte de los patrones funcionales del lenguaje.
Cómo funciona una closure
Cuando una función se define dentro de otra, la función interna mantiene una referencia al scope de la externa. Aunque la función externa ya haya devuelto su resultado, las variables del scope externo siguen vivas mientras la función interna exista:
function crearContador() {
let cuenta = 0; // Variable en el scope de crearContador
return function() {
cuenta++;
return cuenta;
};
}
const contador = crearContador();
console.log(contador()); // 1
console.log(contador()); // 2
console.log(contador()); // 3
// crearContador ya terminó, pero 'cuenta' sigue viva
// porque contador la referencia
El bug clásico de closures con var en bucles
El error más conocido con closures ocurre al crear funciones dentro de un bucle con var. Todas las funciones capturan la misma variable, no valores independientes:
// Con var: todas las funciones ven el mismo 'i'
const funciones = [];
for (var i = 0; i < 3; i++) {
funciones.push(function() { return i; });
}
console.log(funciones[0]()); // 3
console.log(funciones[1]()); // 3
console.log(funciones[2]()); // 3
// Solución 1: usar let (scope de bloque, binding por iteración)
const funcionesLet = [];
for (let j = 0; j < 3; j++) {
funcionesLet.push(function() { return j; });
}
console.log(funcionesLet[0]()); // 0
console.log(funcionesLet[1]()); // 1
console.log(funcionesLet[2]()); // 2
// Solución 2 (clásica, para código legado): IIFE
const funcionesIIFE = [];
for (var k = 0; k < 3; k++) {
funcionesIIFE.push((function(n) {
return function() { return n; };
})(k));
}
Funciones de fábrica con closures
Las closures permiten crear funciones especializadas a partir de una función genérica, capturando parámetros de configuración:
function multiplicador(factor) {
return function(numero) {
return numero * factor;
};
}
const doble = multiplicador(2);
const triple = multiplicador(3);
console.log(doble(5)); // 10
console.log(triple(5)); // 15
// Aplicación real: generador de validadores
function crearValidador(min, max) {
return function(valor) {
return valor >= min && valor <= max;
};
}
const edadValida = crearValidador(0, 120);
const porcentajeValido = crearValidador(0, 100);
console.log(edadValida(25)); // true
console.log(porcentajeValido(150)); // false
Memoización con closures
Una closure puede mantener una caché entre llamadas para evitar recalcular resultados costosos:
function memoizar(fn) {
const cache = new Map();
return function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
console.log('Desde caché');
return cache.get(clave);
}
const resultado = fn(...args);
cache.set(clave, resultado);
return resultado;
};
}
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fibMemo = memoizar(fibonacci);
console.log(fibMemo(10)); // Calcula
console.log(fibMemo(10)); // Desde caché: 55
Módulos con IIFE
Antes de los módulos ES6, el patrón de módulo con IIFE usaba closures para crear ámbito privado y exponer solo lo necesario:
const carrito = (function() {
let items = []; // Privado, no accesible desde fuera
return {
agregar(producto) { items.push(producto); },
eliminar(nombre) {
items = items.filter(i => i.nombre !== nombre);
},
total() {
return items.reduce((sum, i) => sum + i.precio, 0);
},
ver() { return [...items]; }
};
})();
carrito.agregar({ nombre: 'Libro', precio: 20 });
carrito.agregar({ nombre: 'Curso', precio: 50 });
console.log(carrito.total()); // 70
console.log(carrito.items); // undefined privado
Hoy este patrón se sustituye por módulos ES o clases con campos privados, pero entender su funcionamiento ayuda a leer código legado y a comprender por qué los módulos modernos funcionan de la misma manera conceptualmente.
