【问题标题】:let keyword in the for loopfor 循环中的 let 关键字
【发布时间】:2013-05-10 00:31:03
【问题描述】:

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

根据MDNfor 循环中使用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 测试了脚本。

【问题讨论】:

  • TL;DR:第一个代码 sn-p 演示了环境的 JavaScript 实现中的一个错误。也就是说,正确的行为是输出 0、1 和 2,而不是 3、3 和 3。现代浏览器正常工作,运行代码时输出 0、1 和 2。
  • 显然这里有三种不同的作用域:let x = 5; for (let x = 0; x &lt; 10; x++) { let x = 3; console.log(x); }

标签: javascript ecmascript-6


【解决方案1】:

squint 的答案不再是最新的。在ECMA 6 specification 中,指定的行为是在

for(let i;;){}

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

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

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

自从使用let 表达式后,每次迭代都会创建一个新的词法范围,链接到前一个范围。这对使用let 表达式有性能影响,报告为here

【讨论】:

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

我通过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,甚至可以根据需要对其进行操作,而与其他范围内的_is 无关。

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

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

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

【讨论】:

  • +1 包含 Babel 翻译,这真的很有用,谢谢!然而,仅仅因为 Babel 必须使用 wtf hack 来翻译 let 关键字以在旧的不兼容 ES6 的浏览器中工作,这当然并不意味着 ES6 在现代浏览器实现中是古怪的。
  • 同意查理。但除此之外,WTFJS 站着。你为什么要假设循环计数器每次循环都是不同的变量?我想假设它是相同的(在这种情况下,LET 表示 for 循环的本地)。我认为这是最大规模的脑放屁。使 i 相同(但与外部范围不同)的代码现在比添加一行来创建要使用的新变量要困难得多。
  • 这也不代表在 ES6+ 中,for (let i = 0, ....) 创建了一个在 for 循环之外不可见的局部范围的 i。转换后的版本执行 var i ,它是函数范围的。这似乎并不能完全代表 ES6+ 的工作原理。
  • 为了证明这一点,读者可以打开 Babel 链接并将const i = 5 添加到代码顶部。然后 Babel 将 output 三个变量:i_i_i2
【解决方案3】:

恕我直言——首先实现这个 LET(产生你的初始版本的结果)的程序员在理智方面做得正确;他们可能在实施过程中没有看过规范。

使用单个变量更有意义,但范围仅限于 for 循环。特别是因为人们应该可以根据循环中的条件随意更改该变量。

但是等等——你可以改变循环变量。哇!但是,如果您尝试在内部范围内更改它,它现在将不起作用因为它是一个新变量。

我不喜欢我必须做的事情来获得我想要的东西(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 变量的意图是什么。或者如果 SANITY 统治宇宙的话。

所以你的第一个例子现在可以在 FF 中运行;它会产生 0、1、2。您可以将问题称为已修复。我把这个问题称为 WTFJS。

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-17
    • 1970-01-01
    • 2014-12-18
    • 1970-01-01
    • 2011-04-01
    相关资源
    最近更新 更多