javascript - support - para que sirve el let en js
Explicación de `let` y bloqueo del alcance con bucles for (3)
Entiendo que
let
evita declaraciones duplicadas, lo cual es bueno.
let x;
let x; // error!
Las variables declaradas con
let
también se pueden usar en cierres que se pueden esperar
let i = 100;
setTimeout(function () { console.log(i) }, i); // ''100'' after 100 ms
Lo que me cuesta entender es cómo se aplica
let
a los bucles.
Esto parece ser específico para
for
loops.
Considere el problema clásico:
// prints ''10'' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints ''0'' through ''9''
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
¿Por qué funciona
let
en este contexto?
En mi imaginación, aunque solo hay un bloque visible,
for
realidad crea un bloque separado para cada iteración y la declaración
let
se realiza dentro de ese bloque ... pero solo hay una declaración
let
para inicializar el valor.
¿Es esto solo azúcar sintáctico para ES6?
¿Cómo funciona esto?
Entiendo las diferencias entre
var
y
let
y las he ilustrado arriba.
Estoy particularmente interesado en entender por qué las diferentes declaraciones resultan en una salida diferente usando un bucle
for
.
¿Es esto solo azúcar sintáctico para ES6?
No, es más que azúcar sintáctica.
Los detalles sangrientos están enterrados en
§13.6.3.9
CreatePerIterationEnvironment
.
¿Cómo funciona esto?
Si usa esa palabra clave
let
en la instrucción
for
, verificará qué nombres vincula y luego
- cree un nuevo entorno léxico con esos nombres para a) la expresión inicializadora b) cada iteración (antes de evaluar la expresión de incremento)
- copie los valores de todas las variables con esos nombres de uno al siguiente entorno
Su declaración de bucle
for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i));
desugar a un simple
// omitting braces when they don''t introduce a block
var i;
i = 0;
if (i < 10)
process.nextTick(_ => console.log(i))
i++;
if (i < 10)
process.nextTick(_ => console.log(i))
i++;
…
while
for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));
hace "desugar" a los mucho más complicados
// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
i = 0;
__status = {i};
}
{ let {i} = __status;
if (i < 10)
process.nextTick(_ => console.log(i))
__status = {i};
} { let {i} = __status;
i++;
if (i < 10)
process.nextTick(_ => console.log(i))
__status = {i};
} { let {i} = __status;
i++;
…
Encontré esta explicación del libro Explorando ES6 el mejor:
declarar una variable en la cabeza de un bucle for crea un enlace único (espacio de almacenamiento) para esa variable:
const arr = []; for (var i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [3,3,3]
Cada i en los cuerpos de las tres funciones de flecha se refiere al mismo enlace, por lo que todos devuelven el mismo valor.
Si deja declarar una variable, se crea un nuevo enlace para cada iteración de bucle:
const arr = []; for (let i=0; i < 3; i++) { arr.push(() => i); } arr.map(x => x()); // [0,1,2]
Esta vez, cada i se refiere al enlace de una iteración específica y conserva el valor que era actual en ese momento. Por lo tanto, cada función de flecha devuelve un valor diferente.
let
introduce el alcance de bloque y el enlace equivalente, al igual que las funciones crean un alcance con cierre.
Creo que la sección relevante de la especificación es
13.2.1
, donde la nota menciona que las declaraciones son parte de un enlace léxico y ambas viven dentro de un entorno léxico.
La sección
13.2.2
establece que las declaraciones
var
se adjuntan a un entorno variable, en lugar de a un enlace léxico.
La explicación de MDN también respalda esto, indicando que:
Funciona al vincular cero o más variables en el ámbito léxico de un solo bloque de código
sugiriendo que las variables están unidas al bloque, que varía cada iteración que requiere un nuevo enlace léxico (creo, no 100% en ese punto), en lugar del entorno léxico circundante o entorno variable que sería constante durante la duración de la llamada.
En resumen, cuando se usa
let
, el cierre está en el cuerpo del bucle y la variable es diferente cada vez, por lo que debe capturarse nuevamente.
Cuando se usa
var
, la variable está en la función circundante, por lo que no es necesario volver a cerrar y se pasa la misma referencia a cada iteración.
Adaptando su ejemplo para ejecutar en el navegador:
// prints ''10'' 10 times
for (var i = 0; i < 10; i++) {
setTimeout(_ => console.log(''var'', i), 0);
}
// prints ''0'' through ''9''
for (let i = 0; i < 10; i++) {
setTimeout(_ => console.log(''let'', i), 0);
}
Ciertamente muestra la última impresión de cada valor. Si observa cómo Babel transpira esto, produce:
for (var i = 0; i < 10; i++) {
setTimeout(function(_) {
return console.log(i);
}, 0);
}
var _loop = function(_i) {
setTimeout(function(_) {
return console.log(_i);
}, 0);
};
// prints ''0'' through ''9''
for (var _i = 0; _i < 10; _i++) {
_loop(_i);
}
Asumiendo que Babel es bastante conforme, eso coincide con mi interpretación de la especificación.