Tema
Scope y closures
¿Por qué a veces una variable "no existe" aunque la acabas de crear? ¿Cómo hace un contador para recordar su valor entre clics sin usar variables globales? Las respuestas están en dos conceptos fundamentales: el scope (alcance) y los closures (clausuras). Dominarlos te separa de un principiante y te permite entender cómo funcionan muchas librerías por dentro.
Concepto
Scope: ¿dónde vive una variable?
El scope es la zona del código donde una variable existe y se puede usar. Piensa en una casa:
- Lo que está en la calle (scope global) lo puede ver cualquiera.
- Lo que está dentro de la casa (scope de función) sólo lo ven quienes están dentro.
- Lo que está dentro de un cajón (scope de bloque) sólo se ve al abrir ese cajón.
Desde dentro de la casa puedes ver la calle por la ventana, pero desde la calle no puedes ver qué hay en los cajones. Lo de adentro puede ver hacia afuera, pero no al revés.
Closure: una función con memoria
Un closure es una función que recuerda las variables del lugar donde fue creada, incluso después de que ese lugar terminó de ejecutarse. Es como una mochila: cuando una función nace, se lleva en su mochila las variables que tenía a su alrededor, y las conserva siempre.
Los tres tipos de scope
1. Scope global
Variables declaradas fuera de cualquier función o bloque. Se pueden usar en todo el programa.
js
const nombreTienda = 'La Esquina'; // global
function mostrarTienda() {
console.log(nombreTienda); // ✅ la función ve la variable global
}
mostrarTienda(); // "La Esquina"2. Scope de función
Variables declaradas dentro de una función. Sólo existen ahí dentro.
js
function calcularTotal() {
const iva = 0.16; // sólo existe dentro de calcularTotal
return 100 * (1 + iva);
}
calcularTotal();
console.log(iva); // ❌ ReferenceError: iva is not defined3. Scope de bloque
let y const declaradas dentro de llaves { } (un if, un for, etc.) sólo existen dentro de ese bloque.
js
const edad = 20;
if (edad >= 18) {
const mensaje = 'Eres mayor de edad'; // scope de bloque
console.log(mensaje); // ✅
}
console.log(mensaje); // ❌ ReferenceError: mensaje is not defined
for (let i = 0; i < 3; i++) {
// i sólo existe aquí dentro
}
console.log(i); // ❌ ReferenceErrorError común: var ignora los bloques
var sólo respeta el scope de función, no el de bloque. Por eso "se escapa" de los if y for:
js
if (true) {
var fuga = '¡Me escapé!';
let segura = 'Yo me quedo aquí';
}
console.log(fuga); // "¡Me escapé!" 😬
console.log(segura); // ❌ ReferenceError
// El clásico error con var en bucles
for (var j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 100);
}
// Imprime 3, 3, 3 (todas comparten la misma j)
for (let k = 0; k < 3; k++) {
setTimeout(() => console.log(k), 100);
}
// Imprime 0, 1, 2 ✅ (cada vuelta tiene su propia k)Otra razón más para usar siempre let y const.
La cadena de scopes
Cuando usas una variable, JavaScript la busca primero en el scope actual. Si no la encuentra, sube al scope que lo contiene, y así hasta llegar al global. Si tampoco está ahí, lanza un ReferenceError.
js
const moneda = 'MXN'; // nivel 1: global
function crearTicket() {
const cliente = 'Ana'; // nivel 2: función
function imprimirLinea() {
const producto = 'Café'; // nivel 3: función interna
// Busca producto (aquí), cliente (arriba), moneda (más arriba)
console.log(`${cliente} compró ${producto} en ${moneda}`);
}
imprimirLinea();
}
crearTicket(); // "Ana compró Café en MXN"Sombreado (shadowing)
Si declaras una variable con el mismo nombre en un scope interno, "tapa" a la de afuera:
js
const mensaje = 'global';
function prueba() {
const mensaje = 'local'; // sombrea a la global
console.log(mensaje); // "local"
}
prueba();
console.log(mensaje); // "global"Hoisting de funciones
Como viste en Funciones, las declaraciones de función se elevan completas: puedes llamarlas antes de escribirlas. Las expresiones y flechas guardadas en const o let no.
js
saludar(); // ✅ "Hola"
// despedir(); // ❌ ReferenceError: Cannot access 'despedir' before initialization
function saludar() {
console.log('Hola');
}
const despedir = () => console.log('Adiós');| Declaración | ¿Se puede usar antes? |
|---|---|
function nombre() {} | ✅ Sí, completa |
var x = 1 | ⚠️ Existe, pero vale undefined |
let / const | ❌ No (TDZ) |
const f = () => {} | ❌ No (TDZ) |
Closures en acción
Veamos un closure paso a paso:
js
function crearSaludo(saludo) {
// saludo vive en el scope de crearSaludo
return function (nombre) {
// Esta función interna "recuerda" saludo
return `${saludo}, ${nombre}`;
};
}
const saludoFormal = crearSaludo('Buenos días');
const saludoCasual = crearSaludo('¿Qué onda');
// crearSaludo ya terminó, pero las funciones recuerdan su "saludo"
console.log(saludoFormal('Sra. López')); // "Buenos días, Sra. López"
console.log(saludoCasual('Beto')); // "¿Qué onda, Beto"Cada llamada a crearSaludo crea un nuevo scope con su propio saludo, y la función devuelta se lo lleva en su "mochila".
Caso real 1: contador con estado privado
Sin closures, un contador necesitaría una variable global que cualquiera podría modificar por error. Con un closure, la variable queda privada:
js
function crearContador() {
let cuenta = 0; // privada: no se puede tocar desde fuera
return {
incrementar() {
cuenta++;
return cuenta;
},
decrementar() {
cuenta--;
return cuenta;
},
valor() {
return cuenta;
},
};
}
const visitas = crearContador();
visitas.incrementar(); // 1
visitas.incrementar(); // 2
console.log(visitas.valor()); // 2
console.log(visitas.cuenta); // undefined → no hay acceso directo 🔒
// Cada contador es independiente
const likes = crearContador();
likes.incrementar(); // 1
console.log(visitas.valor()); // 2 (no se afectó)Pruébalo: cada tarjeta es un contador creado con una fábrica similar. Agrega más y verás que ninguno afecta a los demás:
El código clave de la demo:
js
function crearContador(paso = 1) {
let cuenta = 0; // cada contador tiene la suya
return {
incrementar: () => (cuenta += paso),
decrementar: () => (cuenta -= paso),
reiniciar: () => (cuenta = 0),
valor: () => cuenta,
};
}
// Cada clic en "Nuevo contador" hace:
const contador = crearContador(paso); // un closure nuevo e independienteCaso real 2: fábricas de funciones
Una fábrica es una función que crea otras funciones ya configuradas:
js
// Fábrica de calculadoras de descuento
function crearDescuento(porcentaje) {
return (precio) => precio - precio * (porcentaje / 100);
}
const descuentoBuenFin = crearDescuento(30);
const descuentoEstudiante = crearDescuento(15);
console.log(descuentoBuenFin(1000)); // 700
console.log(descuentoEstudiante(1000)); // 850
// Fábrica de formateadores de moneda
function crearFormateador(moneda, locale) {
const formato = new Intl.NumberFormat(locale, { style: 'currency', currency: moneda });
return (cantidad) => formato.format(cantidad);
}
const pesos = crearFormateador('MXN', 'es-MX');
const dolares = crearFormateador('USD', 'en-US');
console.log(pesos(1499.5)); // "$1,499.50"
console.log(dolares(1499.5)); // "$1,499.50"Fíjate que formato se crea una sola vez por fábrica y se reutiliza en cada llamada gracias al closure.
Caso real 3: debounce básico
Imagina un buscador que consulta una API cada vez que el usuario teclea. Si escribe "zapatos" serían 7 peticiones. Con debounce esperamos a que deje de escribir un momento y hacemos una sola petición.
js
function debounce(funcion, espera = 300) {
let temporizador; // vive en el closure, compartido entre llamadas
return (...args) => {
clearTimeout(temporizador); // cancelamos la espera anterior
temporizador = setTimeout(() => {
funcion(...args); // ejecutamos tras la pausa
}, espera);
};
}
// Uso: buscar sólo cuando el usuario deja de escribir 400 ms
const buscar = (texto) => console.log(`Buscando "${texto}"...`);
const buscarConDebounce = debounce(buscar, 400);
const input = document.querySelector('#buscador');
input.addEventListener('input', (e) => {
buscarConDebounce(e.target.value);
});¿Por qué funciona? Porque temporizador está en el closure: todas las llamadas a buscarConDebounce comparten la misma variable y pueden cancelar el temporizador anterior. Lo usarás en el proyecto del buscador.
Buena práctica
- Declara las variables en el scope más pequeño posible: si sólo se usan dentro de un
if, decláralas ahí. - Evita variables globales: cualquier parte del código (o un script de terceros) puede modificarlas.
- Usa closures para encapsular estado privado en lugar de exponer variables sueltas.
Error común
Pensar que cada función creada en un closure comparte la misma variable cuando se llama a la fábrica varias veces. Cada llamada a la fábrica crea un scope nuevo: crearContador() dos veces = dos cuenta distintas. Pero dentro de una misma llamada, todas las funciones devueltas (incrementar, valor...) sí comparten la misma variable.
Ejercicios
- ¿Qué imprime este código y por qué?
js
let x = 'afuera';
function prueba() {
let x = 'adentro';
if (true) {
let x = 'bloque';
}
console.log(x);
}
prueba();
console.log(x);- Crea una fábrica
crearMultiplicador(n)que devuelva una función que multiplique cualquier número porn. CreatripleyporDiez. - Crea
crearCuentaBancaria(saldoInicial)que devuelva los métodosdepositar(monto),retirar(monto)(sin permitir saldo negativo) yconsultar(). El saldo debe ser privado. - Crea
unaVez(fn): devuelve una función que ejecutafnsólo la primera vez que se llama; las siguientes no hacen nada (útil para evitar doble clic en "Pagar").
Ver solución
1. Imprime "adentro" y luego "afuera". El x del bloque if sólo existe dentro de sus llaves; el console.log dentro de la función encuentra el x de la función, y el de afuera ve el global.
2.
js
function crearMultiplicador(n) {
return (numero) => numero * n;
}
const triple = crearMultiplicador(3);
const porDiez = crearMultiplicador(10);
console.log(triple(5)); // 15
console.log(porDiez(7)); // 703.
js
function crearCuentaBancaria(saldoInicial = 0) {
let saldo = saldoInicial;
return {
depositar(monto) {
if (monto <= 0) return 'Monto inválido';
saldo += monto;
return saldo;
},
retirar(monto) {
if (monto > saldo) return 'Fondos insuficientes';
saldo -= monto;
return saldo;
},
consultar() {
return saldo;
},
};
}
const cuenta = crearCuentaBancaria(1000);
cuenta.depositar(500); // 1500
cuenta.retirar(2000); // "Fondos insuficientes"
console.log(cuenta.consultar()); // 1500
console.log(cuenta.saldo); // undefined 🔒4.
js
function unaVez(fn) {
let yaSeEjecuto = false;
return (...args) => {
if (yaSeEjecuto) return;
yaSeEjecuto = true;
return fn(...args);
};
}
const pagar = unaVez(() => console.log('💳 Procesando pago...'));
pagar(); // "💳 Procesando pago..."
pagar(); // (nada)
pagar(); // (nada)Resumen
- El scope define dónde existe una variable: global, de función o de bloque.
letyconstrespetan el scope de bloque;varsólo el de función (evítalo).- JavaScript busca las variables subiendo por la cadena de scopes: de adentro hacia afuera.
- Las declaraciones de función tienen hoisting completo; las flechas en
constno. - Un closure es una función que recuerda las variables del lugar donde se creó.
- Usos reales: estado privado (contadores, cuentas), fábricas de funciones y debounce.
Siguiente: Arrays