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

IT技术 javascript ecmascript-6
2021-01-21 09:24:05

我知道这let可以防止重复声明,这很好。

let x;
let x; // error!

用 with 声明的变量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个回答

这只是 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++;
        …

for (let i = 0; i < 10; i++) process.nextTick(_ => console.log(i));“脱糖”是否更复杂

// 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++;
          …
@Bergi 与 let
2021-03-14 09:24:05
@Bergi 喜欢你的回答,但我仍然无法想象执行上下文的样子。如果您有时间,请编辑您的答案以显示这会是什么样子?
2021-03-20 09:24:05
它只是一个包含一个变量的作用域i它由闭包引用,并且引用包含循环的作用域作为其外部链接。
2021-03-24 09:24:05
@KonstantinosDimakis 您指的是哪种上下文?
2021-03-26 09:24:05
这可能比我的回答更正确,也更简洁。
2021-04-01 09:24:05

从探索 ES6 书中找到了最好的解释

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

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

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

如果您让-声明一个变量,则会为每次循环迭代创建一个新绑定:

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

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

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

这个答案比选择的答案更容易理解。
2021-03-14 09:24:05
如果你归因于ivaluearr.length,当然,删除 incrementation i++,它仍然有效吗?
2021-03-27 09:24:05

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

MDN说明支持此也,指出:

它的工作原理是在单个代码块的词法范围内绑定零个或多个变量

建议变量绑定到块,它改变每次迭代需要一个新的 LexicalBinding(我相信,在这一点上不是 100%),而不是周围的 Lexical Environment 或 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);
}

当然显示后者打印每个值。如果你看看 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);
}

假设 Babel 相当一致,这与我对规范的解释相符。

@ExplosionPills 围绕范围(/environments/bindings)的规范往往变得有点迟钝。我仍然很难理解它,但似乎有效的结果是每次迭代都有自己的绑定。Babel/ES6 也不允许你混合let a = 1, const b = 2使用:你只是不能在一个语句中混合不同的声明类型。
2021-03-11 09:24:05
我不得不多次阅读规范——我仍然认为我没有完全理解,但查看 Babel 的输出会有所帮助。本质上 usinglet确实会在每次迭代中创建一个新范围。我想这只是内置于语言中。Babel 也不喜欢混合let i = 0, var j = 1无论如何都不兼容的声明
2021-03-22 09:24:05
@TinyGiant 大多数浏览器无法运行第一个片段,因为它们往往不支持 let 和箭头功能。
2021-04-01 09:24:05