在for循环中使用let关键字

63

ECMAScript 6的let 应该提供块级作用域来避免变量提升带来的问题。但是在下面的代码中,为什么函数中的i解析为循环的最后一个值(就像使用var一样),而不是当前迭代的值?

"use strict";
var things = {};
for (let i = 0; i < 3; i++) {
    things["fun" + i] = function() {
        console.log(i);
    };
}

things["fun0"](); // prints 3
things["fun1"](); // prints 3
things["fun2"](); // prints 3
根据MDN的说明,在for循环中使用let应该会将变量绑定到循环体的作用域。当我在块内部使用临时变量时,事情按照我的预期进行。这样做的必要性是什么?
"use strict";
var things = {};
for (let i = 0; i < 3; i++) {
    let index = i;
    things["fun" + i] = function() {
        console.log(index);
    };
}

things["fun0"](); // prints 0
things["fun1"](); // prints 1
things["fun2"](); // prints 2

我使用 Traceur 和 node --harmony 测试了这个脚本。


1
简而言之:第一个代码片段展示了环境中JavaScript实现的一个错误。正确的行为是输出0、1和2,而不是3、3和3。当你运行代码时,现代浏览器会正确地输出0、1和2。 - Utku
显然这里有三个不同的作用域: let x = 5; for (let x = 0; x < 10; x++) { let x = 3; console.log(x); } - joeytwiddle
3个回答

74

squint的答案已经过时了。在ECMA 6规范中,指定的行为是在

for(let i;;){}

i在循环的每次迭代中都会获得一个新的绑定。

这意味着每个闭包都捕获了不同的i实例。因此,当前结果012是正确的。当您在Chrome v47+中运行它时,您会得到正确的结果。但是当您在IE11和Edge中运行它时,目前似乎会产生错误的结果(333)。

有关此Bug/功能的更多信息可以在此页面中的链接中找到;

使用let表达式后,每次迭代都会创建一个新的词法作用域链接到前一个作用域。这对于使用let表达式具有性能影响,该问题在此处报告。


16
在循环的每次迭代中,i 都是一个独立的实例,但是对 i 的任何修改仍然会影响循环的迭代次数。这是一个奇怪的东西,既是独立变量的一半,又是对原始变量的引用的一半。 - jfriend00
3
在每次新的迭代开始时,都会建立一个新的词法作用域,并复制上一个作用域中 "i" 的值。 - neuron
6
知道了,但这不是单纯的复制,因为在循环期间修改它仍然会影响循环变量。因此,可能是在每次循环执行结束时将其值复制回循环计数器。 - jfriend00
3
我的理解是:在代码块开头,会将变量i的值复制到一个新的本地变量中,你可以使用标识符i引用它。在该块结束时,它将被复制回循环变量中,在下一次迭代中再次复制回去,以此类推。(实现方式还有更高效的方法,但这是我所能想到的最简单的方式) - anon
5
出于两个原因,我希望对这条留言进行回溯:第一,可能会对其他人有所帮助;第二,当时我写留言的时候没有注意到日期。 - anon
显示剩余3条评论

22

我将这段代码通过Babel转换,以便我们能够用熟悉的ES5语法理解其行为:

for (let i = 0; i < 3; i++) {
    i++;
    things["fun" + i] = function() {
        console.log(i);
    };
    i--;
}

以下是转译为 ES5 的代码:

var _loop = function _loop(_i) {
    _i++;
    things["fun" + _i] = function () {
        console.log(_i);
    };
    _i--;
    i = _i;
};

for (var i = 0; i < 3; i++) {
    _loop(i);
}

我们可以看到使用了两个变量。

  • 在外部作用域中,i 是随着迭代而改变的变量。

  • 在内部作用域中,_i 是每次迭代的唯一变量。最终会有三个不同的 _i 实例。

    每个回调函数都能看到其对应的 _i,并且甚至可以独立于其他作用域中的 _i 进行操作。

    (您可以通过在回调函数中执行 console.log(i++) 来确认存在三个不同的 _i。在较早的回调中更改 _i 不会影响后续回调的输出。)

在每次迭代结束时,将 _i 的值复制到 i 中。因此,在迭代期间更改内部唯一变量将影响外部迭代变量。

很高兴看到 ES6 继续了 WTFJS 的悠久传统。


3
感谢您包括了 Babel 翻译,它对于我们查看非英语内容非常有用!然而,Babel 必须使用一些奇怪的技巧来将 let 关键字翻译为在旧的、不符合 ES6 标准的浏览器中工作,这并不意味着现代浏览器实现中 ES6 是不好的。 - charlie roberts
1
同意Charlie的观点。但是此外,WTFJS的立场还存在。为什么您想假设循环计数器每次通过循环都是不同的变量?我希望它是相同的(在这种情况下,LET表示for循环的局部变量)。我认为这是一个非常严重的失误。现在使i相同(但与外部作用域不同)的代码比添加一行创建新变量要困难得多,以供使用。 - Gerard ONeill
这也并不表示在ES6+中,for(let i = 0, ....)创建了一个仅在for循环内部可见的局部作用域的i。转译版本会使用var i,它将是函数作用域的。这似乎并不完全代表ES6+的工作方式。 - jfriend00
为了证明这一点,读者可以打开Babel链接并在代码顶部添加const i = 5。然后,Babel将输出三个变量:i_i_i2 - joeytwiddle

4
在我看来,最初实现 LET 的程序员(生成初始版本的结果)在理智方面做得很正确;他们可能没有在实现过程中瞥一眼规范。
使用单个变量但限定于 for 循环范围更有意义。特别是当你可以根据循环内的条件自由更改该变量时。
但等等——你可以更改循环变量。WTFJS!然而,如果你试图在内部范围内更改它,现在它将不起作用,因为它是一个新变量。
我不喜欢我所必须做的事情,以获取我想要的东西(一个只在 for 循环中本地的单个变量)。
{
    let x = 0;
    for (; x < length; x++)
    {
        things["fun" + x] = function() {
            console.log(x);
        };
    }
}

如果要修改更直观(虽然是想象的)版本以处理每次迭代的新变量,则需要:

for (let x = 0; x < length; x++)
{
    let y = x;
    things["fun" + y] = function() {
        console.log(y);
    };
}

如果SANITY统治这个世界的话,我的y变量的意图是显然的。但是现在,你可以在Firefox上运行你的第一个例子,并获得0, 1, 2的结果。你可以称之为问题已解决,而我则认为这就是WTFJS(令人困惑的JavaScript)。

顺便说一句,我的WTFJS的提到是来自JoeyTwiddle。这听起来像一个我今天应该知道的迷因,但今天学习它也是很好的时机。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接