En la unidad anterior viste una función que devolvía otra función y que, de alguna forma, seguía recordando un valor de fuera. No es un truco: es la consecuencia directa de cómo busca JavaScript las variables. Entender el ámbito y los closures te explicará comportamientos que, de otro modo, parecen magia o errores del lenguaje.
1 // Qué es el ámbito
El ámbito (scope) de una variable es la zona del código desde la que se puede usar. En JavaScript moderno hay tres niveles:
const app = 'Tienda'; // ámbito global: visible en todo el script
function calcularTotal(precios) {
let total = 0; // ámbito de función: sólo dentro de calcularTotal
for (const precio of precios) {
const conIva = precio * 1.21; // ámbito de bloque: sólo dentro de este for
total += conIva;
}
// console.log(conIva); // ReferenceError: aquí ya no existe
return total;
}Con let y const, cualquier par de llaves crea un ámbito nuevo: el cuerpo de un if, de un for o un bloque suelto. Esto es igual que en Java, así que te resultará natural.
Hay un detalle adicional: aunque la variable pertenece a todo su bloque, no se puede usar antes de su declaración. Si lo intentas, obtienes un ReferenceError. A esa franja entre el inicio del bloque y la declaración se la llama zona muerta temporal.
Todo esto vale para let y const. La forma antigua de declarar, var, sigue otras reglas, y conviene verlas porque la encontrarás en código antiguo y en muchos ejemplos de internet:
function mostrarNotas(notas) {
for (var i = 0; i < notas.length; i++) {
var nota = notas[i];
if (nota >= 5) {
var mensaje = 'Hay algún aprobado';
}
}
console.log(i); // 3: i sigue existiendo fuera del for
console.log(nota); // 8: nota también
console.log(mensaje); // 'Hay algún aprobado': y mensaje, fuera del if y del for
}
mostrarNotas([4, 7, 8]);Con let, cada una de esas tres líneas finales lanzaría ReferenceError, porque las variables sólo existían dentro de sus llaves. Con var, el único límite es la función: da igual en qué bloque se declare.
La segunda rareza es que una variable var se puede usar antes de la línea que la declara. El motor registra la declaración al entrar en la función (el mismo hoisting que viste con las funciones), pero el valor no se asigna hasta llegar a su línea:
console.log(total); // undefined: no hay error
var total = 10;
console.log(total); // 10
console.log(precio); // ReferenceError: Cannot access 'precio' before initialization
let precio = 10;| Aspecto | let y const |
var |
|---|---|---|
| Ámbito | El bloque: las llaves más cercanas | La función entera |
| Uso antes de declarar | ReferenceError |
Vale undefined, sin error |
| Declarar dos veces el mismo nombre | Error de sintaxis | Se permite en silencio |
En un bucle for |
Una variable nueva por vuelta | Una sola variable para todo el bucle |
var no respeta los bloques: una variable declarada con var dentro de un if o un for es visible en toda la función. Además, se puede usar antes de declararla y vale undefined sin dar error. Es la razón principal por la que en código moderno no se usa var.
2 // La cadena de ámbitos
Los ámbitos se anidan como cajas unas dentro de otras. Cuando el código usa una variable, el motor la busca siguiendo siempre el mismo recorrido:
- En el ámbito actual.
- Si no está, en el ámbito que lo contiene.
- Y así hacia fuera, hasta el ámbito global.
- Si no aparece en ninguno, lanza
ReferenceError.
La búsqueda va siempre de dentro hacia fuera, nunca al revés: una función interior ve las variables de la exterior, pero la exterior no ve las de la interior.
Lo decisivo es que esta cadena se decide por dónde está escrita la función en el código, no por desde dónde se llama. Por eso se habla de ámbito léxico: basta con leer el código y ver qué llaves rodean a cada función para saber qué variables puede usar.
3 // Closures: funciones con memoria
Junta ahora dos ideas: una función puede devolver otra función, y cada función puede usar las variables de los ámbitos que la rodean. ¿Qué pasa cuando la función exterior termina?
function crearContador() {
let cuenta = 0;
return () => {
cuenta++;
return cuenta;
};
}
const contadorA = crearContador();
const contadorB = crearContador();
contadorA(); // 1
contadorA(); // 2
contadorB(); // 1 (otra cuenta distinta)
contadorA(); // 3En Java, las variables locales de un método desaparecen al terminar. Aquí, crearContador ya ha terminado, pero la función devuelta todavía necesita cuenta, así que el motor la conserva.
Esa es la definición: un closure (en español, clausura) es una función junto con las variables del ámbito en el que se creó. Toda función de JavaScript queda enganchada al sitio donde nació, y mantiene vivas las variables de ese sitio que usa mientras la propia función exista, aunque el código que las declaró haya terminado hace rato.
Una imagen que ayuda: cada función lleva una mochila con las variables de fuera que usa. Cuando crearContador devuelve la flecha, la flecha sale con la mochila puesta, y dentro va cuenta.
Tres detalles completan la idea:
- Cada llamada crea una mochila nueva. Llamar a
crearContador()crea un ámbito nuevo, con su propiacuenta. Por esocontadorAycontadorBno se interfieren. - La mochila guarda la variable, no una copia de su valor. Si la función cambia
cuenta, la siguiente llamada ve el cambio; por eso el contador avanza. - Desde fuera no se puede abrir. No existe
contadorA.cuenta: la única forma de llegar a esa variable es llamar a la función.
Un closure permite tener estado privado sin clases: cuenta sólo se puede modificar a través de la función devuelta. Nadie más en el programa puede ponerla a cero o cambiarla por error.
4 // Dónde vas a encontrar closures
No vas a escribir crearContador todos los días, pero sí vas a usar closures continuamente sin darte cuenta. Cada vez que un callback usa una variable de fuera, hay un closure:
function configurarBoton(boton, mensaje) {
let pulsaciones = 0;
boton.addEventListener('click', () => {
pulsaciones++;
console.log(`${mensaje} (${pulsaciones})`);
});
}La función del click se ejecutará mucho después de que configurarBoton termine, y aun así tendrá acceso a mensaje y pulsaciones. Lo mismo ocurre con los callbacks de setTimeout, con las promesas o con los componentes de Vue.
Un bucle programa tres mensajes con setTimeout para que aparezcan uno por segundo:
for (var i = 1; i <= 3; i++) {
setTimeout(() => console.log(`Mensaje ${i}`), i * 1000);
}El resultado es Mensaje 4 tres veces.
- Cuándo se ejecutan los callbacks:
setTimeoutno espera; programa la función y el bucle sigue. Los tres callbacks se ejecutan cuando el bucle ya terminó. - Cuántas
ihay: convar, la variable pertenece a la función entera, así que existe una solaicompartida por los tres closures. - Cuánto vale al final: el bucle sale cuando
illega a4. Esa es laique ven los tres callbacks. - Qué cambia con
let:letcrea una variable nueva en cada vuelta del bucle. Cada callback cierra sobre laide su vuelta.
var por let se obtiene Mensaje 1, Mensaje 2 y Mensaje 3. El código no cambia de lógica: cambia cuántas variables hay para que los closures recuerden.5 // Para practicar
Predice qué valores imprime un script que mezcla var y let, y compruébalo después. Cada ejercicio trae el enunciado en los comentarios del código.
Sombreado de variables y var en buclestraining-daw-sv/modulo-00/04-ambito-y-closures/01_scopesAutoevaluación
Una función devuelta por crearContador() sigue usando la variable cuenta aunque crearContador ya terminó. ¿Por qué no se ha perdido esa variable?
cuenta, el motor no la elimina. Eso es un closure: una función junto con las variables del lugar donde se definió.Dos contadores creados con dos llamadas a crearContador() no comparten la cuenta. ¿Qué lo explica?
cuenta. La primera función devuelta se queda con la cuenta de la primera llamada y la segunda, con la de la segunda. Son dos variables distintas que se llaman igual.Un bucle for (var i = 0; i < 3; i++) programa tres setTimeout que muestran i, y los tres muestran 3. Con let muestran 0, 1 y 2. Explica la diferencia.
var crea una sola variable para todo el bucle; let crea una por vuelta. Los callbacks se ejecutan cuando el bucle ya terminó. Con var los tres ven la misma i, que ya vale 3; con let cada callback se quedó con la i de su vuelta.