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

IT技术 javascript var let
2021-01-18 19:06:51

此代码记录66 次:

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

这就是 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在评论中发布了 Babel.js 的链接。查看实时演示的评论中的链接,您可以在其中更改代码中的任何内容并立即观看翻译。看看其他 ES6 特性是如何被翻译的,这很有趣。

为了补充您的出色解释,这里是babeljs.io 提供的 ES6 代码到 ES5 的翻译
2021-03-13 19:06:51
范围在这里无关紧要,这取决于每次迭代时创建的新绑定
2021-03-18 19:06:51
@MichaelGeary 感谢您的链接。我在答案中添加了 Babel 翻译。谢谢。
2021-03-21 19:06:51

从技术上讲,这就是@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 被记录了六次。