Closures y Ámbito Léxico
Comprender cómo los closures capturan y preservan el ámbito, su funcionamiento interno y aplicaciones prácticas
¿Qué son los Closures?
Un closure es una función que tiene acceso a variables en su ámbito externo, incluso después de que la función externa haya terminado de ejecutarse. Los closures son fundamentales para JavaScript y permiten patrones poderosos como privacidad de datos, fábricas de funciones y currying.
Conceptos Clave
- Ámbito Léxico — Las funciones tienen acceso a variables definidas en su ámbito externo
- Preservación del Estado — Los closures "recuerdan" su entorno de creación
- Privacidad de Datos — Encapsula variables que no son accesibles desde el exterior
- Fábricas de Funciones — Crea funciones con comportamiento preconfigurado
Ámbito Léxico Básico
function exterior() {
const mensaje = "Hola desde exterior";
function interior() {
// Tiene acceso a 'mensaje' desde el ámbito exterior
console.log(mensaje);
}
interior(); // "Hola desde exterior"
}
exterior();
// El closure en acción
function hacerSaludo(nombre) {
const saludo = "Hola";
return function() {
// Esta función es un closure
// "Recuerda" 'saludo' y 'nombre' incluso después de que hacerSaludo() termine
console.log(`${saludo}, ${nombre}!`);
};
}
const saludarAlice = hacerSaludo("Alice");
const saludarBob = hacerSaludo("Bob");
saludarAlice(); // "Hola, Alice!"
saludarBob(); // "Hola, Bob!"
Ejemplo del Mundo Real: Privacidad de Datos
function crearCuentaBancaria(saldoInicial) {
// Variable privada - no accesible desde el exterior
let saldo = saldoInicial;
// API pública a través de closures
return {
depositar(monto) {
if (monto > 0) {
saldo += monto;
console.log(`Depositado: $${monto}. Nuevo saldo: $${saldo}`);
}
},
retirar(monto) {
if (monto > 0 && monto <= saldo) {
saldo -= monto;
console.log(`Retirado: $${monto}. Saldo restante: $${saldo}`);
} else {
console.log("Fondos insuficientes");
}
},
obtenerSaldo() {
return saldo;
}
};
}
const miCuenta = crearCuentaBancaria(100);
miCuenta.depositar(50); // Depositado: $50. Nuevo saldo: $150
miCuenta.retirar(30); // Retirado: $30. Saldo restante: $120
console.log(miCuenta.obtenerSaldo()); // 120
// No se puede acceder a 'saldo' directamente
console.log(miCuenta.saldo); // undefined
Fábrica de Funciones
// Crear funciones especializadas usando closures
function crearMultiplicador(factor) {
return function(numero) {
return numero * factor;
};
}
const duplicar = crearMultiplicador(2);
const triplicar = crearMultiplicador(3);
const cuadruplicar = crearMultiplicador(4);
console.log(duplicar(5)); // 10
console.log(triplicar(5)); // 15
console.log(cuadruplicar(5)); // 20
// Ejemplo práctico: formateo con closure
function crearFormateadorMoneda(simbolo, decimales = 2) {
return function(monto) {
return `${simbolo}${monto.toFixed(decimales)}`;
};
}
const formatoUSD = crearFormateadorMoneda("$");
const formatoEUR = crearFormateadorMoneda("€");
const formatoYEN = crearFormateadorMoneda("¥", 0);
console.log(formatoUSD(42.5)); // "$42.50"
console.log(formatoEUR(42.5)); // "€42.50"
console.log(formatoYEN(42.5)); // "¥43"
Memoización con Closures
// Cachear resultados de funciones costosas
function memoize(fn) {
const cache = {}; // Variable privada en el closure
return function(...args) {
const clave = JSON.stringify(args);
if (clave in cache) {
console.log("Desde caché:", clave);
return cache[clave];
}
const resultado = fn(...args);
cache[clave] = resultado;
return resultado;
};
}
// Función costosa
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const fibMemo = memoize(fibonacci);
console.log(fibMemo(10)); // Calcula
console.log(fibMemo(10)); // Desde caché
console.log(fibMemo(11)); // Calcula solo nuevos valores
Errores Comunes: Closures en Bucles
// ❌ PROBLEMA: var y closures en bucles
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Imprime: 3, 3, 3 (no 0, 1, 2)
}, 100);
}
// Problema: todos los closures comparten la misma variable 'i'
// ✅ SOLUCIÓN 1: Usar 'let' (ámbito de bloque)
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Imprime: 0, 1, 2 ✓
}, 100);
}
// ✅ SOLUCIÓN 2: IIFE (Expresión de Función Inmediatamente Invocada)
for (var i = 0; i < 3; i++) {
(function(index) {
setTimeout(function() {
console.log(index); // Imprime: 0, 1, 2 ✓
}, 100);
})(i);
}
// ✅ SOLUCIÓN 3: Parámetro de función
for (var i = 0; i < 3; i++) {
setTimeout(function(index) {
console.log(index); // Imprime: 0, 1, 2 ✓
}, 100, i);
}
Closures en Manejadores de Eventos
// Problema: perder contexto en callbacks
function configurarBotones() {
for (var i = 1; i <= 3; i++) {
const boton = document.getElementById(`btn${i}`);
// ❌ MALO: closure sobre 'i' mutable
boton.addEventListener('click', function() {
console.log(`Botón ${i} clicado`); // i será 4 para todos
});
}
}
// ✅ BUENO: usar let o crear un closure apropiado
function configurarBotones() {
for (let i = 1; i <= 3; i++) {
const boton = document.getElementById(`btn${i}`);
boton.addEventListener('click', function() {
console.log(`Botón ${i} clicado`); // Funciona correctamente
});
}
}
// O con fábrica de funciones
function crearManejadorClick(indice) {
return function() {
console.log(`Botón ${indice} clicado`);
};
}
for (var i = 1; i <= 3; i++) {
const boton = document.getElementById(`btn${i}`);
boton.addEventListener('click', crearManejadorClick(i));
}
💡 Puntos Clave
- • Los closures permiten que las funciones accedan a variables de ámbitos externos
- • Son perfectos para privacidad de datos y encapsulación
- • Las fábricas de funciones usan closures para crear funciones especializadas
- • Cuidado con
varen bucles - usaleto IIFEs - • Los closures se usan ampliamente en callbacks, manejadores de eventos y programación funcional
- • Cada función forma un closure sobre su ámbito léxico
Ejercicios rápidos de práctica
- • Crea un contador privado con métodos
incrementar()yobtenerValor()usando closures. - • Escribe una función memoize que cachee resultados de funciones costosas.
- • Construye una fábrica de funciones que cree temporizadores con diferentes intervalos.
- • Soluciona el problema clásico del bucle adjuntando event listeners únicos a múltiples elementos.