使用for循环

时间:2015-06-17 18:32:19

标签: javascript ecmascript-6

我理解let可以防止重复声明,这很好。

let x;
let x; // error!

使用let声明的变量也可用于可预期的闭包

let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms

我有点难以理解的是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)) }

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

我理解varlet之间的差异,并在上面说明了这些差异。我特别感兴趣的是理解为什么不同的声明使用for循环导致不同的输出。

3 个答案:

答案 0 :(得分:34)

  

这只是ES6的语法糖吗?

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

  

这是如何工作的?

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

  • 创建一个新的词汇环境,其中包含以下名称: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++;
        …

for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));确实&#34; desugar&#34;更复杂的

// 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++;
          …

答案 1 :(得分:7)

我发现this explanation from Exploring ES6 book是最好的:

  

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

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

三箭头函数体中的每一个都指的是相同的   绑定,这就是为什么它们都返回相同的值。

     

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

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

arr.map(x => x()); // [0,1,2]
     

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

答案 2 :(得分:6)

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

MDN explanation也支持这一点,并说明:

  

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

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

简而言之,当使用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);
}

肯定会显示后者打印每个值。如果你看看巴贝尔如何解释这一点,就会产生:

&#13;
&#13;
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);
}
&#13;
&#13;
&#13;

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