javascript - ¿Por qué los enlaces let y var se comportan de manera diferente usando la función setTimeout?
hoisting (2)
Con
var
tiene un alcance de función, y solo un enlace compartido para todas sus iteraciones de bucle, es decir, el
i
en cada devolución de llamada setTimeout significa
la misma
variable que
finalmente
es igual a 6 después de que finaliza la iteración del bucle.
Al
let
tener un alcance de bloque y cuando se usa en el bucle
for
, obtiene un nuevo enlace para cada iteración, es decir, el
i
en cada devolución de llamada setTimeout significa
una
variable
diferente
, cada una de las cuales tiene un valor diferente: el primero es 0, el el siguiente es 1 etc.
Así que esto:
(function timer() {
for (let i = 0; i <= 5; i++) {
setTimeout(function clog() { console.log(i); }, i * 1000);
}
})();
es equivalente a esto usando solo var:
(function timer() {
for (var j = 0; j <= 5; j++) {
(function () {
var i = j;
setTimeout(function clog() { console.log(i); }, i * 1000);
}());
}
})();
usando la expresión de función invocada inmediatamente para usar el alcance de la función de manera similar a como el alcance del bloque funciona en el ejemplo con
let
.
Podría escribirse más corto sin usar el nombre
j
, pero quizás no sería tan claro:
(function timer() {
for (var i = 0; i <= 5; i++) {
(function (i) {
setTimeout(function clog() { console.log(i); }, i * 1000);
}(i));
}
})();
E incluso más corto con las funciones de flecha:
(() => {
for (var i = 0; i <= 5; i++) {
(i => setTimeout(() => console.log(i), i * 1000))(i);
}
})();
(Pero si puede usar las funciones de flecha, no hay razón para usar
var
.)
Así es como Babel.js traduce su ejemplo con
let
para ejecutar en entornos donde
let
no está disponible:
"use strict";
(function timer() {
var _loop = function (i) {
setTimeout(function clog() {
console.log(i);
}, i * 1000);
};
for (var i = 0; i <= 5; i++) {
_loop(i);
}
})();
Gracias a Michael Geary por publicar el enlace a Babel.js en los comentarios. Vea el enlace en el comentario para una demostración en vivo donde puede cambiar cualquier cosa en el código y ver la traducción que se realiza de inmediato. Es interesante ver cómo se traducen también otras funciones de ES6.
Esta pregunta ya tiene una respuesta aquí:
- ¿Cuál es la diferencia entre usar "let" y "var"? 32 respuestas
- Explicación de `let` y bloqueo del alcance con for loops 3 respuestas
Este código registra
6
, 6 veces:
(function timer() {
for (var i=0; i<=5; i++) {
setTimeout(function clog() {console.log(i)}, i*1000);
}
})();
Pero este código ...
(function timer() {
for (let i=0; i<=5; i++) {
setTimeout(function clog() {console.log(i)}, i*1000);
}
})();
... registra el siguiente resultado:
0
1
2
3
4
5
¿Por qué?
¿Es porque
let
une al alcance interno de cada elemento de manera diferente y
var
mantiene el último valor de
i
?
Técnicamente es como @rsp explica en su excelente respuesta.
Así es como me gusta entender que las cosas funcionan debajo del capó.
Para el primer bloque de código usando
var
(function timer() {
for (var i=0; i<=5; i++) {
setTimeout(function clog() {console.log(i)}, i*1000);
}
})();
Puedes imaginar que el compilador va así dentro del ciclo for
setTimeout(function clog() {console.log(i)}, i*1000); // first iteration, remember to call clog with value i after 1 sec
setTimeout(function clog() {console.log(i)}, i*1000); // second iteration, remember to call clog with value i after 2 sec
setTimeout(function clog() {console.log(i)}, i*1000); // third iteration, remember to call clog with value i after 3 sec
y así
como se declara usando
var
, cuando se llama a
clog
, el compilador encuentra la variable
i
en el bloque de función más cercano que es el
timer
y como ya hemos llegado al final del ciclo
for
, mantengo el valor 6 y ejecuto
clog
.
Eso explica que 6 se registre seis veces.