Perché lasciare e le associazioni var si comportano diversamente usando la funzione setTimeout?

Questo codice registra 6 , 6 volte:

 (function timer() { for (var i=0; i<=5; i++) { setTimeout(function clog() {console.log(i)}, i*1000); } })(); 

Ma questo codice …

 (function timer() { for (let i=0; i<=5; i++) { setTimeout(function clog() {console.log(i)}, i*1000); } })(); 

… registra il seguente risultato:

 0 1 2 3 4 5 

Perché?

È perché let bind allo scopo interno ogni elemento in modo diverso e var mantiene l’ultimo valore di i ?

Con var hai un ambito di funzione, e solo un binding condiviso per tutte le iterazioni del ciclo – cioè l’ i in ogni callback setTimeout indica la stessa variabile che alla fine è uguale a 6 dopo che l’iterazione del ciclo termina.

Con l’ let a portata di blocco e una volta utilizzata nel ciclo for si ottiene una nuova associazione per ogni iterazione, ovvero l’ i in ogni callback setTimeout indica una variabile diversa , ognuna delle quali ha un valore diverso: la prima è 0, la il prossimo è 1 ecc.

Così questo:

 (function timer() { for (let i = 0; i <= 5; i++) { setTimeout(function clog() { console.log(i); }, i * 1000); } })(); 

è equivalente a questo usando solo var:

 (function timer() { for (var j = 0; j <= 5; j++) { (function () { var i = j; setTimeout(function clog() { console.log(i); }, i * 1000); }()); } })(); 

utilizzando l'espressione di funzione immediatamente invocata per utilizzare l'ambito della funzione in un modo simile a come l'ambito del blocco funziona nell'esempio con let .

Potrebbe essere scritto più breve senza usare il nome j , ma forse non sarebbe così chiaro:

 (function timer() { for (var i = 0; i <= 5; i++) { (function (i) { setTimeout(function clog() { console.log(i); }, i * 1000); }(i)); } })(); 

E ancora più corto con le funzioni di freccia:

 (() => { for (var i = 0; i <= 5; i++) { (i => setTimeout(() => console.log(i), i * 1000))(i); } })(); 

(Ma se puoi usare le funzioni freccia, non c'è motivo di usare var .)

In questo modo Babel.js traduce il tuo esempio con let da eseguire in ambienti in cui let non è disponibile:

 "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); } })(); 

Grazie a Michael Geary per aver pubblicato il link a Babel.js nei commenti. Vedere il link nel commento per una demo dal vivo in cui è ansible modificare qualsiasi cosa nel codice e guardare la traduzione che si svolge immediatamente. È interessante vedere come vengono tradotte anche le altre funzionalità di ES6.

Tecnicamente è come @spsp spiega nella sua eccellente risposta. Questo è il modo in cui mi piace capire che le cose funzionano sotto il cofano. Per il primo blocco di codice usando var

 (function timer() { for (var i=0; i<=5; i++) { setTimeout(function clog() {console.log(i)}, i*1000); } })(); 

Potete immaginare che il compilatore vada in questo modo all'interno 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 

e così via

dato che i viene dichiarato usando var , quando viene chiamato clog , il compilatore trova la variabile i nel blocco funzione più vicino che è timer e poiché abbiamo già raggiunto la fine del ciclo for , i detengo il valore 6 ed eseguo l' clog . Ciò spiega che sei registrato sei volte.