【问题标题】:'setTimeOut' calls in JavaScript 'for' loops, why do they fail? [duplicate]JavaScript 'for' 循环中的'setTimeOut' 调用,为什么它们会失败? [复制]
【发布时间】:2017-12-31 06:45:11
【问题描述】:

让我澄清一下我的问题。我不是在问如何使以下代码工作。我知道您可以使用 let 关键字或 iffe 来捕获它自己的 i 值。我只需要说明如何在以下代码中访问值i。我阅读了以下博客文章,了解以下代码如何不起作用。 Blog post

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。也就是说,闭包在作用域中看起来是回调已经结束了,在这种情况下,这也是 6,因为它是在 for 循环完成之后完成的。

它是哪一个,该函数是根据在每次迭代中作为引用传递的变量还是因为词法作用域而将 i 的值解析为 6?

【问题讨论】:

  • 有趣的是,如果你设置超时时间为 0 毫秒,你仍然会得到 66666!
  • @JonMarkPerry 那是因为 JS 是单线程的。当计时器触发时,超时被添加到消息队列中(即暂停只是最小延迟)。 MDN 在the Event Loop 上有一篇很棒的文章,您可以在其中阅读它
  • @Kiren; vg,所以我们在执行 sT 之前等待 for...loop 队列清空,此时 i 为 6,除非我们使用闭包。
  • 你可以使用setTimeout(console.log(eval(i)));
  • "然后他们根据词法范围规则查找变量 i。" - 如果没有对所述词法范围的引用,这怎么可能? (当然,无论是对整个作用域的引用还是对单个变量的引用,以及“按规则查找”何时完成,都是实现细节,需要优化)。

标签: javascript settimeout


【解决方案1】:

你是正确的,词法范围是这种行为的原因。当计时器函数运行时(将在当前运行的代码完成之后),它们会尝试解析i,并且必须查找scope chain 才能找到它。由于词法作用域,i 在作用域链中仅存在一次(比计时器函数高一个作用域),此时,i6,因为此时循环已终止。

var 关键字使 JavaScript 中的变量具有函数或全局范围(基于声明所在的位置)。在您的代码中,var i 导致 i 变量全局存在(因为您的代码不在函数内部),并且每个计时器函数必须在最终运行时解析相同的单个 i。由于计时器函数在循环完成之前不会运行,i 处于循环导致它的最后一个值 (6)。

var i改为let ito create block scope换成i即可解决问题。

let 为变量创建块范围。在循环的每次迭代中,您再次进入循环块,并为 i 创建一个单独的范围,每个计时器函数都可以访问它自己。

for (let i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}

【讨论】:

  • 出于这个原因,我已经完全停止使用 var。对于大多数 c 语法风格的语言来说,函数作用域变量不是标准。正如答案所说,让强制执行块范围。
  • let 也没有被提升以供参考 OP。有很多与 var 相关的内部包袱。使用时要小心。
  • 谢谢。我知道解决方案。我只是想澄清i 的值是如何解决的。是否已解决抛出词法范围?当回调函数被触发时,它们是否会通过词法范围查找i 的值。由于回调在外部范围内对变量 i 进行了封闭,因此 i 的值是否在词法上得到解析。这意味着回调函数将首先在其范围内查找值,然后在外部范围内查找?谢谢
  • @HugoPerea 是的,JavaScript 使用词法作用域。闭包是这个的副产品。
  • 升级了。第一段可能是我读过的最令人满意的简洁和清晰的段落,并且未经编辑,很容易应用于同一个问题的永无止境的变化。
【解决方案2】:

让我用你的代码解释一下:

for (var i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}

在函数setTimeout()被触发的那一刻,i的变量将等于你所期望的1,2,3,4,5,直到i的值增加到6并且停止 for 循环。

   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的值。看看上面,正如我所说,i 的值已经是 6。

    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 &lt;=5 ;i++) 将创建一个变量,该变量将存在于整个函数中,并且可以在局部作用域或闭包作用域内被函数修改。这就是我们在 ECMAScript 6 中使用 let 的原因。

var改为let即可轻松修复:

for (let i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}

【讨论】:

  • 谢谢。我知道这是如何工作的,我只需要帮助理解我所询问的部分。
猜你喜欢
  • 2016-12-26
  • 1970-01-01
  • 1970-01-01
  • 2020-04-18
  • 1970-01-01
  • 1970-01-01
  • 2017-10-04
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多