web-dev-qa-db-ja.com

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が各項目の内部スコープに異なる方法でバインドし、variの最新の値を保持するためですか?

49
user2290820

varを使用すると、関数スコープがあり、すべてのループ反復で1つの共有バインディングのみが使用されます。つまり、すべてのsetTimeoutコールバックのi同じ変数finallyは、ループの反復が終了すると6に等しくなります。

letを使用すると、ブロックスコープがあり、forループで使用すると、各反復の新しいバインディングが取得されます。つまり、すべてのsetTimeoutコールバックのi異なる変数で、それぞれが異なる値を持っています:最初のものは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へのリンクを投稿していただいた Michael Geary に感謝します。コメント内のリンクを参照して、コード内のすべてを変更し、変換がすぐに行われるのを見ることができるライブデモを確認してください。他のES6機能がどのように翻訳されるかを見るのは興味深いです。

43
rsp

技術的には、@ 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

等々

ivarを使用して宣言されているため、clogが呼び出されると、コンパイラーは変数itimerである最も近い関数ブロックで見つけます。すでにforループの終わりに達し、iは値6を保持し、clogを実行します。つまり、6が6回ログに記録されることになります。

7
Quannt