使用for循环解释`let`和块作用域

Exp*_*lls 31 javascript ecmascript-6

据我所知,这let可以防止重复的声明,这很好.

let x;
let x; // error!
Run Code Online (Sandbox Code Playgroud)

声明的变量let也可以用在可以预期的闭包中

let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms
Run Code Online (Sandbox Code Playgroud)

我有点难以理解的是如何let应用于循环.这似乎是for循环特有的.考虑一下经典问题:

// prints '10' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints '0' through '9'
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
Run Code Online (Sandbox Code Playgroud)

为什么let在这种情况下使用起作用?在我的想象中,即使只有一个块是可见的,for实际上为每个迭代创建一个单独的块,并且let声明在该块内完成......但是只有一个let声明来初始化该值.这只是ES6的语法糖吗?这是怎么回事?

我理解上面和之间的差异var,let并在上面说明了它们.我特别感兴趣的是理解为什么不同的声明使用for循环导致不同的输出.

Ber*_*rgi 39

这只是ES6的语法糖吗?

不,它不仅仅是语法糖.血淋淋的细节埋藏在§13.6.3.9中 CreatePerIterationEnvironment.

这是怎么回事?

如果letfor语句中使用该关键字,它将检查它绑定的名称然后

  • 创建一个新的词法环境,其中包含以下命名:a)初始化表达式b)每次迭代(主要用于评估增量表达式)
  • 将所有变量的值与这些名称从一个环境复制到下一个环境

你的循环语句for (var i = 0; i < 10; i++) process.nextTick(_ => console.log(i));很简单

// omitting braces when they don't introduce a block
var i;
i = 0;
if (i < 10)
    process.nextTick(_ => console.log(i))
    i++;
    if (i < 10)
        process.nextTick(_ => console.log(i))
        i++;
        …
Run Code Online (Sandbox Code Playgroud)

虽然for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));"desugar"更加复杂

// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
  i = 0;
  __status = {i};
}
{ let {i} = __status;
  if (i < 10)
      process.nextTick(_ => console.log(i))
      __status = {i};
}   { let {i} = __status;
      i++;
      if (i < 10)
          process.nextTick(_ => console.log(i))
          __status = {i};
    }   { let {i} = __status;
          i++;
          …
Run Code Online (Sandbox Code Playgroud)


swa*_*hra 9

我发现探索ES6的这个解释是最好的:

var声明for循环头部的变量会为该变量创建单个绑定(存储空间):

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]
Run Code Online (Sandbox Code Playgroud)

三个箭头函数的主体中的每个i都指向相同的绑定,这就是它们都返回相同值的原因.

如果允许声明变量,则为每个循环迭代创建一个新绑定:

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}

arr.map(x => x()); // [0,1,2]
Run Code Online (Sandbox Code Playgroud)

这一次,每个i指的是一个特定迭代的绑定,并保留当时当前的值.因此,每个箭头函数返回一个不同的值.


ssu*_*ube 6

let引入了块作用域和等效绑定,就像函数创建带闭包的作用域一样.我相信规范的相关部分是13.2.1,其中注释提到let声明是LexicalBinding的一部分,并且都存在于词法环境中.第13.2.2规定,var声明连接到VariableEnvironment,而不是LexicalBinding.

MDN说明支持此也,指出:

它通过在单个代码块的词法范围内绑定零个或多个变量来工作

建议变量绑定到块,这会改变需要新的LexicalBinding的每次迭代(我相信,在该点上不是100%),而不是周围的词汇环境或VariableEnvironment,它在调用期间是恒定的.

简而言之,在使用时let,闭包位于循环体上,每次变量都不同,因此必须再次捕获.使用时var,变量位于周围函数中,因此不需要重新闭合,并且将相同的引用传递给每次迭代.

调整您的示例以在浏览器中运行:

// prints '10' 10 times
for (var i = 0; i < 10; i++) {
  setTimeout(_ => console.log('var', i), 0);
}

// prints '0' through '9'
for (let i = 0; i < 10; i++) {
  setTimeout(_ => console.log('let', i), 0);
}
Run Code Online (Sandbox Code Playgroud)

当然显示后者打印每个值.如果你看看Babel如何解释这个,它会产生:

for (var i = 0; i < 10; i++) {
  setTimeout(function(_) {
    return console.log(i);
  }, 0);
}

var _loop = function(_i) {
  setTimeout(function(_) {
    return console.log(_i);
  }, 0);
};

// prints '0' through '9'
for (var _i = 0; _i < 10; _i++) {
  _loop(_i);
}
Run Code Online (Sandbox Code Playgroud)

假设Babel相当一致,那就符合我对规范的解释.