'setTimeOut' calls in JavaScript 'for' loops, why do they fail?
让我澄清一下我的问题。我不是问如何使以下代码工作。我知道你可以使用let关键字或iffe来捕获它自己的i值。我只需要澄清如何在以下代码中访问值i。我阅读了以下博客文章,了解以下代码是如何工作的。博客文章
1 2 3
| for (var i = 1; i <= 5; i++) {
setTimeout(function() { console.log(i); }, 1000*i); // 6 6 6 6 6
} |
作者声称代码不起作用,因为我们将变量i作为引用而不是值传递。也就是说,我们不是每次迭代提供i的值,而是将变量作为参考提供给setTimeout中的回调。实际上,当循环终止并且回调触发时,我们将引用变量i,它将是6.这是如何工作的?
这是我的理解。我的理解是,当执行循环时,我们不会将任何内容"传递"到setTimeout函数的回调中。我们只是设置异步调用。当闭包回调函数执行时,它们会根据词法作用域规则查找变量i。也就是说,在范围内的闭包是回调结束了,在这种情况下再次,因为它是在for循环完成之后完成的。
它是哪一个,函数是否根据在每次迭代时作为引用传递的变量或者因为词法作用域而将i的值解析为6?
-
有趣的是,如果setTimeout暂停0ms,你仍然会得到66666!
-
@JonMarkPerry那是因为JS是单线程的。当计时器触发时,超时被添加到消息队列中(即,暂停仅是最小延迟)。 MDN在Event Loop上有一篇很棒的文章,你可以在其中阅读它
-
@Kiren; vg,所以我们等待for ...循环队列在sT执行之前清除,到那时i是6,除非我们使用闭包。
-
你可以使用setTimeout(console.log(eval(i)));
-
"然后他们根据词汇范围规则寻找变量i。" - 如果不提??及所述词汇范围,怎么可能呢? (当然,无论引用是针对整个范围还是单个变量,以及何时完成"根据规则查找",都是实现细节并且需要进行优化)。
-
@JonMarkPerry是的,但是在99.99999%的用例中应该避免使用eval() - 当然这个。
-
有一个对范围的引用..我问的是直接引用变量i ...
你是正确的,词法范围是这种行为的原因。当计时器功能运行时(将在当前运行的代码完成之后),它们会尝试解析i并且他们必须查找范围链以找到它。由于词法作用域,i在作用域链中只存在一次(一个作用域高于计时器函数),并且此时i是6,因为此时循环已终止。
var关键字使JavaScript中的变量具有函数或全局范围(基于声明的位置)。在您的代码中,var i导致i变量全局存在(因为您的代码不在函数内),并且每个定时器函数必须在它们最终运行时解析相同的单个i。由于定时器函数在循环完成之前不会运行,因此i处于循环导致它的最后一个值(6)。
将var i更改为let i以创建i的块范围以解决问题。
let为变量创建块范围。在循环的每次迭代中,您再次进入循环块,并为i创建一个单独的范围,每个计时器函数都会自行获取。
1 2 3
| for (let i = 1; i <= 5; i++) {
setTimeout(function() { console.log(i); }, 1000*i);
} |
-
出于这个原因,我已经完全停止使用var。当涉及到大多数c语法风格的语言时,函数范围的变量不是常态。正如答案所说,让我们强制执行块范围。
-
也不会因为参考OP而被吊起。 var有很多内部包袱。使用时要小心。
-
谢谢。我知道解决方案。我只想澄清如何解决i的值。是否解决了抛出词汇范围?当回调函数被触发时,它们会通过词法范围查找i的值。由于回调对外部作用域中的变量i进行了闭包,因此i的值是否会被词法解析。这意味着回调函数将首先在其范围内查找值,然后在外部范围内查找?谢谢
-
@HugoPerea是的,JavaScript使用词法作用域。闭包是这种副产品。
-
你是什??么意思"引用"。那是我的问题。当你说每个回调引用相同的变量i时,这意味着当函数执行时,它不需要查找变量throw lexical scope,因为它已经有一个对它的引用。
-
@HugoPerea否。必须查找范围链以解析i。这就是词法范围的意思。让我把"参考"改为"解决"。
-
啊,这就是我想知道的。谢谢
-
@simon let和const也被悬挂
-
不,他们不是。
-
无论该文章中的其他人说什么,吊装都需要声明/和/初始化。如果没有给出值,则vars和let都被初始化为undefined,但是只有var才能在声明之前被访问。以下是参考资料:dmitripavlutin.com/…
-
在浏览器中解析的示例:z = 5;的console.log(Z);让z;如果let被挂起,console.log()会输出它。
-
@PeterMortensen请限制对更正的修改。在提到全球范围时使用大写"G"是很常见的,这是我的意图。
-
@Bergi它在MDN文档中指出let:"在ECMAScript 2015中,让绑定不受变量提升的影响,这意味着让声明不会移动到当前执行上下文的顶部。"
-
@ScottMarcus显然MDN在使用术语"吊装"方面不一致。
-
@Bergi或者,您可能只是说他们的文档中存在错误并且修复了它。
-
up'ed。第一段也许是我读过的最令人满意的简洁和清晰,并且未经编辑,很容易应用于同一问题的永无止境的变化。
让我用你的代码解释一下:
1 2 3
| for (var i = 1; i <= 5; i++) {
setTimeout(function() { console.log(i); }, 1000*i);
} |
在触发函数setTimeout()时,i的变量将等于您预期的1,2,3,4,5,直到i的值增加到6并停止for循环。
1 2 3 4 5 6 7 8 9 10 11 12
| var i = 1;
setTimeout(function() { console.log(i); }, 1000*1);
i++;
setTimeout(function() { console.log(i); }, 1000*2);
i++;
setTimeout(function() { console.log(i); }, 1000*3);
i++;
setTimeout(function() { console.log(i); }, 1000*4);
i++;
setTimeout(function() { console.log(i); }, 1000*5);
i++;
// Now i = 6 and stop the for-looping. |
一段时间后,将触发timeout的回调,并执行i的控制台日志值。看看上面,正如我所说,我的价值已经是6。
1 2 3 4 5
| console.log(i) // i = 6 already.
console.log(i) // i = 6 already.
console.log(i) // i = 6 already.
console.log(i) // i = 6 already.
console.log(i) // i = 6 already. |
原因是缺乏ECMAScript 5:block scope。 (var i = 1;i <=5 ;i++)将创建一个将存在于整个函数中的变量,并且可以通过本地范围或闭包范围中的函数进行修改。这就是为什么我们在ECMAScript&nbsp; 6中有let的原因。
通过将var更改为let可以轻松修复:
1 2 3
| for (let i = 1; i <= 5; i++) {
setTimeout(function() { console.log(i); }, 1000*i);
} |
-
谢谢。我知道这是如何工作的,我只需要帮助理解我所询问的部分。