for 循环中的 let 关键字

IT技术 javascript ecmascript-6
2021-01-29 17:11:51

ECMAScript 6let应该提供块作用域而不会带来麻烦。有人可以解释为什么在下面的代码i中函数解析为循环中的最后一个值(就像 with 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

根据MDNletfor循环中使用,应该将变量绑定在循环体的范围内。当我在块内使用临时变量时,事情会像我期望的那样工作。为什么这是必要的?

"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.

3个回答

眯眼的答案不再是最新的。ECMA 6 规范中,指定的行为是

for(let i;;){}

i 为循环的每次迭代获取一个新的绑定。

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

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

因为当使用let表达式,每次迭代都会创建一个新的词法作用域,链接到前一个作用域。这对使用let表达式的性能有影响,此处报告

奇怪的是,虽然它是一个单独的实例,如果i在循环的每次迭代中,循环i内的任何修改仍然会影响循环的迭代计数。这是一个奇怪的野兽,一半是独立的变量,一半是对原著的引用。
2021-03-14 17:11:51
@jfriend00 在每次新的迭代开始时,建立一个新的词法作用域,并将之前作用域中“i”的值复制过来。
2021-03-14 17:11:51
@jfriend00 我的理解是这样的:在代码块的开头, 的值i被复制到一个新的局部变量中,该变量是您使用 identifier 引用的i在该块的末尾,它被复制回循环变量,在下一次迭代中被复制回,等等(有一种更有效的方法来实现它,但这是我想到的最简单的方法)
2021-03-14 17:11:51
对,我知道。但是,它不是纯副本,因为在循环期间修改它仍然会以某种方式影响循环变量。因此,它必须类似于在每次循环执行结束时将其值复制回循环计数器。
2021-03-28 17:11:51
@jfriend00 (a) 追溯,因为它可能有助于其他人阅读本文,以及 (b) 因为我没有注意到我写评论的日期。
2021-03-30 17:11:51

我通过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其他作用域中s无关

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

在每次迭代结束时, 的值_i被复制到 中i因此在迭代过程中改变唯一的内部变量会影响外部迭代变量。

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

这也不代表在 ES6+ 中,for (let i = 0, ....)创建了一个在循环i外不可见的局部作用域for转译后的版本做了一个var i函数作用域。这似乎并不能完全代表 ES6+ 的工作方式。
2021-03-30 17:11:51
+1 包含 Babel 翻译,这非常有用,谢谢!然而,仅仅因为 Babel 必须使用 wtf hacks 将 let 关键字转换为在旧的非 ES6 兼容浏览器中工作当然并不意味着 ES6 在现代浏览器实现中是古怪的。
2021-04-07 17:11:51
同意查理。但除此之外,WTFJS 还站着。为什么要假设循环计数器每次循环都是不同的变量?我想假设它是相同的(在这种情况下,LET 将指示 for 循环的本地)。我认为这是最大的脑放屁。使 i 变得相同(但与外部作用域不同)的代码现在比添加一行来创建要使用的新变量要困难得多。
2021-04-07 17:11:51
为了证明这一点,读者可以打开 Babel 链接并添加const i = 5到代码的顶部。Babel 将输出三个变量:i,_i_i2
2021-04-09 17:11:51

恕我直言——首先实现这个 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);
    };
}

很清楚我对 y 变量的意图是什么......或者如果理智统治宇宙的话。

所以你的第一个例子现在适用于 FF;它产生 0, 1, 2。你可以称问题已修复。我称这个问题为 WTFJS。

附:我对 WTFJS 的引用来自上面的 JoeyTwiddle;这听起来像是我今天之前应该知道的模因,但今天是学习它的好时机。