关于javascript:为什么使用setTimeout函数时let和var绑定的行为不同?

Why let and var bindings behave differently using setTimeout function?

本问题已经有最佳答案,请猛点这里访问。

此代码记录6,6次:

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

但是这个代码…

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

…记录以下结果:

1
2
3
4
5
6
0
1
2
3
4
5

为什么?

是因为let对内部范围的约束不同,var保持i的最新值吗?


使用var时,您有一个函数范围,并且对于所有循环迭代,只有一个共享绑定——即,每个setTimeout回调中的i表示循环迭代结束后最终等于6的相同变量。

使用let有一个块作用域,当在for循环中使用时,每次迭代都会得到一个新的绑定,即每个setTimeout回调中的i表示一个不同的变量,每个变量都有不同的值:第一个是0,下一个是1等。

因此:

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

仅使用var就等于:

1
2
3
4
5
6
7
8
(function timer() {
  for (var j = 0; j <= 5; j++) {
    (function () {
      var i = j;
      setTimeout(function clog() { console.log(i); }, i * 1000);
    }());
  }
})();

使用立即调用的函数表达式使用函数作用域的方式与在使用let的示例中使用块作用域的方式类似。

它可以不用j的名字写得更短,但可能不那么清楚:

1
2
3
4
5
6
7
(function timer() {
  for (var i = 0; i <= 5; i++) {
    (function (i) {
      setTimeout(function clog() { console.log(i); }, i * 1000);
    }(i));
  }
})();

甚至更短的箭头功能:

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

(但如果您可以使用arrow函数,就没有理由使用var)。

这就是babel.js如何将您的示例与let转换为在let不可用的环境中运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
"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在评论中发布了babel.js的链接。请查看评论中的链接,了解实时演示,您可以在其中更改代码中的任何内容,并立即观看翻译过程。看到其他ES6特性如何被翻译也是很有趣的。


从技术上讲,这就是@rsp在他出色的回答中的解释。这就是我喜欢理解在引擎盖下工作的方式。对于使用var的第一个代码块

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

您可以想象编译器在for循环中是这样运行的

1
2
3
 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次被记录。