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 un poco entender es cómo let se aplica a los bucles. Esto parece ser específico for bucles. 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 el uso de let en este contexto? En mi imaginación, aunque solo se ve un bloque, 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 dan como resultado una salida diferente usando un bucle for .
¿Es esto solo azúcar sintáctico para ES6?
No, es más que azúcar sintáctico. Los detalles sangrientos están enterrados en §13.6.3.9 CreatePerIterationEnvironment .
¿Cómo funciona esto?
Si usa esa palabra clave let en la declaración for , verificará qué nombres vincula y luego
Su instrucción de bucle for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i)); desazúcares 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 lo mucho mas complicado
// 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 Exploring ES6 la mejor:
var-declarar una variable en el encabezado 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 todas devuelven el mismo valor.
Si declara una variable, se crea un nuevo enlace para cada iteración del 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 actual en ese momento. Por lo tanto, cada función de flecha devuelve un valor diferente.
let introduce el alcance del bloque y el enlace equivalente, al igual que las funciones crean un alcance con el cierre. Creo que la sección relevante de la especificación es 13.2.1 , donde la nota menciona que las declaraciones let son parte de un LexicalBinding y ambas viven dentro de un Lexical Environment. La sección 13.2.2 establece que las declaraciones de var se adjuntan a un entorno variable, en lugar de a un enlace léxico.
La explicación de MDN también respalda esto, afirmando que:
Funciona vinculando cero o más variables en el ámbito léxico de un solo bloque de código.
lo que sugiere que las variables están vinculadas al bloque, que varía cada iteración y requiere un nuevo LexicalBinding (creo que no al 100% en ese punto), en lugar del Lexical Environment o VariableEnvironment circundante, 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 este último imprimiendo cada valor. Si observa cómo Babel transpila 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.