Closures y Scope
El scope determina qué variables son accesibles en cada punto del código. Los closures son el resultado natural del scope léxico: una función que recuerda las variables del entorno donde fue creada, aunque ese entorno ya no esté en la pila de llamadas.
Scope léxico
JavaScript usa scope léxico: el scope de una variable se determina en el momento de escribir el código, no de ejecutarlo. Una función puede acceder a las variables del bloque donde fue definida, no del bloque donde fue llamada:
const mensaje = 'global';
function externa() {
const mensaje = 'externa';
function interna() {
// interna puede acceder a las variables de externa y del global
console.log(mensaje); // 'externa' — el scope léxico gana
}
interna();
}
externa();
La cadena de scope (scope chain)
Cuando JavaScript busca una variable, sube por la cadena de scopes hasta encontrarla o llegar al global:
const x = 1; // global
function nivel1() {
const y = 2;
function nivel2() {
const z = 3;
// nivel2 ve: z (propio), y (nivel1), x (global)
console.log(x, y, z); // 1 2 3
}
// nivel1 ve: y (propio), x (global) — NO ve z
nivel2();
}
Closures
Un closure ocurre cuando una función retorna otra función (o la guarda para usarla después). La función interna cierra sobre las variables de su entorno, manteniéndolas vivas:
function crearContador() {
let cuenta = 0; // privado — no accesible desde fuera
return {
incrementar() { cuenta++ },
decrementar() { cuenta-- },
valor() { return cuenta },
};
}
const contador = crearContador();
contador.incrementar();
contador.incrementar();
contador.incrementar();
console.log(contador.valor()); // 3
// `cuenta` está viva porque las funciones retornadas la referencian
// pero no es accesible directamente — esto es encapsulación
console.log(contador.cuenta); // undefined
Factory functions
Las closures permiten crear “fábricas” que generan funciones personalizadas:
function multiplicador(factor) {
return (numero) => numero * factor;
}
const doble = multiplicador(2);
const triple = multiplicador(3);
console.log(doble(5)); // 10
console.log(triple(5)); // 15
// Cada función guarda su propia copia de `factor`
function saludar(saludo) {
return (nombre) => `${saludo}, ${nombre}!`;
}
const saludarEnEspañol = saludar('Hola');
const saludarEnIngles = saludar('Hello');
console.log(saludarEnEspañol('Ana')); // 'Hola, Ana!'
console.log(saludarEnIngles('Ana')); // 'Hello, Ana!'
Memoización
Un closure puede cachear resultados de operaciones costosas:
function memoizar(fn) {
const cache = new Map(); // el Map vive en el closure
return function(...args) {
const clave = JSON.stringify(args);
if (cache.has(clave)) {
console.log('cache hit');
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);
fibMemo(40); // tarda
fibMemo(40); // instantáneo — devuelve del cache
El problema de var en bucles
Un error clásico que ilustra perfectamente cómo funcionan los closures:
// ❌ Con var — un solo binding compartido
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Imprime: 3 3 3
// Por qué: var tiene scope de función, no de bloque
// Cuando los timers se ejecutan, el bucle ya terminó e i vale 3
// ✅ Con let — un binding por iteración
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// Imprime: 0 1 2
// Por qué: let crea un nuevo scope por cada iteración del bucle
IIFE (Immediately Invoked Function Expression)
La forma antigua de crear scope privado antes de que existieran let/const y los módulos. Todavía aparece en código legado:
(function() {
// Todo lo declarado aquí es privado
const privado = 'no contamina el global';
console.log(privado);
})();
// Con arrow function
(() => {
const privado = 'también funciona';
})();
Hoy en día los módulos ES hacen lo mismo de forma más explícita.
Closures en el navegador: event listeners
function configurarBoton(elemento, mensaje) {
// `mensaje` queda encerrado en el closure del listener
elemento.addEventListener('click', () => {
alert(mensaje);
});
}
configurarBoton(document.querySelector('#btn1'), 'Clic en botón 1');
configurarBoton(document.querySelector('#btn2'), 'Clic en botón 2');
// Cada botón tiene su propio `mensaje` encerrado
Precaución: closures y memoria
Los closures mantienen vivas las variables referenciadas. Si una función guarda una referencia a un elemento DOM o un conjunto grande de datos, ese objeto no puede ser recolectado por el garbage collector:
// ❌ Puede causar memory leaks si se acumulan muchos listeners
function adjuntarListener(el) {
const datosPesados = cargarMuchosKB(); // gran objeto
el.addEventListener('click', () => {
console.log(datosPesados); // el closure mantiene datosPesados vivo
});
}
// ✅ Extraer solo lo necesario
function adjuntarListener(el) {
const id = cargarMuchosKB().id; // solo guarda el id
el.addEventListener('click', () => {
console.log(id); // closure pequeño
});
}
En la última lección del curso aprendemos a manejar errores de forma robusta con try/catch, tipos de error y cómo crear errores personalizados.