Почему привязки let и var ведут себя по-разному с помощью функции setTimeout? [дубликат]

этот вопрос уже есть ответ здесь:

этот код журналы 6, 6 раз:

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

но этот код...

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

... регистрирует следующее результат:

0
1
2
3
4
5

почему?

потому что let привязывает к внутренней области каждый элемент по-разному и var сохраняет последнее значение i?

2 ответов


С var у вас есть область функции и только одна общая привязка для всех итераций цикла-т. е. i в каждом setTimeout обратный вызов означает тот же переменная наконец-то равно 6 после завершения итерации цикла.

С let у вас есть объем блока и при использовании в for петли вы получаете новую привязку для каждой итерации, т. е. i в каждом обратном вызове setTimeout значит другое переменная, каждая из которых имеет разное значение: первая-0, следующая-1 и т. д.

значит так:

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

эквивалентно этому, используя только var:

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

использование немедленно вызванного выражения функции для использования области функции аналогично тому, как область блока работает в Примере с let.

это может быть написано короче без использования j имя, но, возможно, это не будет как ясно:

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

и еще короче с функциями стрелки:

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

(но если вы можете использовать функции со стрелками, нет причин использовать var.)

вот как Вавилон.js переводит ваш пример с let для работы в средах, где let не имеется:

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

спасибо Майкл Гири для размещения ссылки на Babel.js в комментариях. См. ссылку в комментарии для живой демонстрации, где вы можете измените что-нибудь в коде и наблюдайте, как перевод происходит немедленно. Интересно посмотреть, как переводятся и другие функции ES6.


технически это то, как @rsp объясняет в своем отличном ответе. Вот как мне нравится понимать, что вещи работают под капотом. Для первого блока кода используется var

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

вы можете себе представить, что компилятор идет так внутри цикла 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

и так далее

С i объявляется с помощью var, когда clog вызывается, компилятор находит переменную i в ближайшем функциональном блоке, который является timer и так как мы уже дошли до конца for цикл i содержит значение 6 и execute clog. Это объясняет, что 6 регистрируется шесть раз.