Waarom laten en var bindingen zich anders gedragen met de setTimeout functie?

Deze code registreert 6, 6 keer:

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

Maar deze code…

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

… registreert het volgende resultaat:

0
1
2
3
4
5

Waarom?

Is het omdat letelk item anders aan de binnenste scope bindt en varde laatste waarde van ibehoudt?


Antwoord 1, autoriteit 100%

Met varheb je een functiebereik en slechts één gedeelde binding voor al je lus-iteraties – dwz de iin elke setTimeout callback betekent dezelfdevariabele die uiteindelijkgelijk is aan 6 nadat de lusiteratie eindigt.

Met letheb je een blokbereik en bij gebruik in de for-lus krijg je een nieuwe binding voor elke iteratie – dwz de iin elke setTimeout callback betekent een anderevariabele, die elk een andere waarde hebben: de eerste is 0, de volgende is 1 enz.

Dus dit:

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

is hier gelijk aan met alleen var:

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

onmiddellijk aangeroepen functie-expressie gebruiken om functiebereik op een vergelijkbare manier te gebruiken als het blokbereik in het voorbeeld met letwerkt.

Het zou korter kunnen worden geschreven zonder de naam jte gebruiken, maar misschien zou het niet zo duidelijk zijn:

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

En nog korter met pijlfuncties:

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

(Maar als u pijlfuncties kunt gebruiken, is er geen reden om varte gebruiken.)

Zo vertaalt Babel.js uw voorbeeld met letom te draaien in omgevingen waar letniet beschikbaar is:

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

Met dank aan Michael Gearyvoor het plaatsen van de link naar Babel.js in de reacties. Zie de link in de opmerking voor een live demo waar je alles in de code kunt veranderen en de vertaling direct kunt zien plaatsvinden. Het is interessant om te zien hoe andere ES6-functies ook worden vertaald.


Antwoord 2, autoriteit 13%

Technisch is het hoe @rsp uitlegt in zijn uitstekende antwoord. Dit is hoe ik graag begrijp dat dingen onder de motorkap werken. Voor het eerste codeblok met var

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

Je kunt je voorstellen dat de compiler zo gaat in de for-lus

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

en ga zo maar door

aangezien iis gedeclareerd met var, vindt de compiler de variabele iin wanneer iwordt aangeroepen het dichtstbijzijnde functieblok dat timeris en aangezien we het einde van de for-lus al hebben bereikt, heeft ide waarde 6 en voert u clog. Dat verklaart dat 6 zes keer is ingelogd.

Other episodes