为什么在nodejs的for循环中让var慢于var?

Jan*_*sch 24 javascript v8 node.js ecmascript-6

我写了一个非常简单的基准:

console.time('var');
for (var i = 0; i < 100000000; i++) {}
console.timeEnd('var')


console.time('let');
for (let i = 0; i < 100000000; i++) {}
console.timeEnd('let')
Run Code Online (Sandbox Code Playgroud)

如果你正在运行Chrome,可以在这里试试(因为NodeJS和Chrome使用相同的JavaScript引擎,虽然版本通常略有不同):

// Since Node runs code in a function wrapper with a different
// `this` than global code, do that:
(function() {
  console.time('var');
  for (var i = 0; i < 100000000; i++) {}
  console.timeEnd('var')


  console.time('let');
  for (let i = 0; i < 100000000; i++) {}
  console.timeEnd('let')
}).call({});
Run Code Online (Sandbox Code Playgroud)

结果令我惊讶:

var: 89.162ms
let: 320.473ms
Run Code Online (Sandbox Code Playgroud)

我在Node 4.0.0 && 5.0.0 && 6.0.0中测试了它,var并且let每个节点版本之间的比例和相同.

有人可以向我解释一下这个看似奇怪的行为的原因是什么?

Pat*_*rts 16

基于varvs 的机制之间的差异let,它与var存在于匿名函数的整个块范围中的事实相关,而let仅存在于循环内并且必须为每次迭代重新声明.1这是一个证明这一点的例子:

(function() {
  for (var i = 0; i < 5; i++) {
    setTimeout(function() {
      console.log(`i: ${i} seconds`);
    }, i * 1000);
  }
  // 5, 5, 5, 5, 5


  for (let j = 0; j < 5; j++) {
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, 5000 + j * 1000);
  }
  // 0, 1, 2, 3, 4
}());
Run Code Online (Sandbox Code Playgroud)

请注意,它i在循环的所有迭代中共享,而let不是.根据您的基准测试,似乎node.js没有优化范围规则,let因为它比它更新近和复杂var.

这里有一个关于letin for循环的小外行解释,对于那些不关心公认密集规范的人,但是好奇的let是如何在保持连续性的同时重新声明每次迭代.

但是let不可能为每次迭代重新声明,因为如果你在循环中更改它,它会传播到下一次迭代!

首先,这是一个几乎可以验证这个潜在反驳论点的例子:

(function() {
  for (let j = 0; j < 5; j++) {
    j++; // see how it skips 0, 2, and 4!?!?
    setTimeout(function() {
      console.log(`j: ${j} seconds`);
    }, j * 1000);
  }
}());
Run Code Online (Sandbox Code Playgroud)

你是部分正确的,因为变化尊重了它的连续性j.但是,正如Babel所证明的那样,它仍然会为每次迭代重新声明:

"use strict";

(function () {
  var _loop = function _loop(_j) {
    _j++; // here's the change inside the new scope
    setTimeout(function () {
      console.log("j: " + _j + " seconds");
    }, _j * 1000);
    j = _j; // here's the change being propagated back to maintain continuity
  };

  for (var j = 0; j < 5; j++) {
    _loop(j);
  }
})();
Run Code Online (Sandbox Code Playgroud)

就像它说的那样.复杂的规则.毫无疑问,基准测试表现出如此大的性能差异(目前).希望将来能够进一步优化.


1:在Babel的REPL上看到这个转换后的版本,看看这个证明了.letfor循环中声明变量时会发生什么,创建一个新的声明性环境来保存该变量(此处 详细信息),然后为每个循环迭代创建另一个声明性环境以保存变量的每个迭代副本; 每个迭代的副本都是从前一个的值初始化的(这里详细信息),但它们是单独的变量,如链接中由闭包输出的值所示.