为什么 let 和 var 绑定在使用 setTimeout 函数时表现不同?

Why let and var bindings behave differently using setTimeout function?

此代码记录 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 的最新值?

使用 var 你有一个函数作用域,并且所有循环迭代只有一个共享绑定 - 即每个 setTimeout 回调中的 i表示相同的变量最后在循环迭代结束后等于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);
  }
})();

感谢 Michael Geary 在评论中将 link 发布到 Babel.js。请参阅评论中的 link 以获取实时演示,您可以在其中更改代码中的任何内容并立即观看翻译。看看其他 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时,编译器会在最近的功能块timer中找到变量i ] 因为我们已经到达了 for 循环的末尾,所以 i 保持值 6,并执行 clog。这就解释了 6 被记录了六次。