Подтвердить что ты не робот

Почему let и var bindings ведут себя по-разному с помощью функции 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?

4b9b3361

Ответ 1

С 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.)

Вот как Babel.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.

Ответ 2

Технически это, как объясняет @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 и выполняет clog. Это объясняет, что 6 регистрируются шесть раз.