web-dev-qa-db-fra.com

Pourquoi les liaisons let et var se comportent-elles différemment en utilisant la fonction setTimeout?

Ce code enregistre 6, 6 fois:

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

Mais ce code ...

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

... enregistre le résultat suivant:

0
1
2
3
4
5

Pourquoi?

Est-ce parce que let se lie à l'étendue interne de chaque élément différemment et var conserve la dernière valeur de i?

49
user2290820

Avec var vous avez une portée de fonction, et une seule liaison partagée pour toutes vos itérations de boucle - c'est-à-dire le i dans chaque rappel setTimeout signifie la même variable que enfin est égale à 6 après la fin de l'itération de la boucle.

Avec let vous avez une portée de bloc et lorsqu'il est utilisé dans la boucle for vous obtenez une nouvelle liaison pour chaque itération - c'est-à-dire que le i dans chaque rappel setTimeout signifie une variable différente , chacune ayant une valeur différente: la première est 0, le suivant est 1 etc.

Donc ça:

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

est équivalent à ceci en utilisant uniquement var:

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

en utilisant l'expression de fonction immédiatement invoquée pour utiliser la portée de la fonction de la même manière que la portée du bloc fonctionne dans l'exemple avec let.

Il pourrait être écrit plus court sans utiliser le nom j, mais ce ne serait peut-être pas aussi clair:

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

Et encore plus court avec les fonctions flèches:

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

(Mais si vous pouvez utiliser les fonctions fléchées, il n'y a aucune raison d'utiliser var.)

Voici comment Babel.js traduit votre exemple avec let pour s'exécuter dans des environnements où let n'est pas 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);
  }
})();

Merci à Michael Geary d'avoir publié le lien vers Babel.js dans les commentaires. Voir le lien dans le commentaire pour une démonstration en direct où vous pouvez changer quoi que ce soit dans le code et regarder la traduction en cours immédiatement. Il est intéressant de voir comment les autres fonctionnalités d'ES6 sont également traduites.

43
rsp

Techniquement, c'est comme l'explique @rsp dans son excellente réponse. C'est ainsi que j'aime comprendre que les choses fonctionnent sous le capot. Pour le premier bloc de code utilisant var

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

Vous pouvez imaginer que le compilateur va comme ceci à l'intérieur de la boucle 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

etc

puisque i est déclaré à l'aide de var, lorsque clog est appelé, le compilateur trouve la variable i dans le bloc fonctionnel le plus proche qui est timer et puisque nous avons déjà atteint la fin de la boucle for, i contient la valeur 6 et exécutons clog. Cela explique 6 étant connecté six fois.

7
Quannt