JavaScript中'for'循环中的'setTimeOut'调用为什么会失败?

8

让我澄清我的问题。我不是在问如何使以下代码工作。我知道你可以使用let关键字或捕获自己值的iffe i。我只需要澄清以下代码中如何访问值i。我读了以下博客文章,了解到以下代码为什么不起作用。博客文章

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循环完成后执行的,因此其值为6。
到底是哪个原因导致函数将变量i的值解析为6?是因为在每次迭代中将变量作为引用传递还是因为词法作用域?

有趣的是,如果你将setTimeout的暂停时间设为0毫秒,你仍然会得到66666! - JMP
@Kiren;好的,那我们等待for...loop队列清空后再执行sT,到那时‘i’就会是6,除非我们使用闭包。 - JMP
你可以使用 setTimeout(console.log(eval(i))); - JMP
“他们根据词法作用域规则查找变量i。” - 如果没有对该词法作用域的引用,这怎么可能实现呢?(当然,引用是整个作用域还是单个变量,以及何时进行“按规则查找”的操作,都是实现细节,可以进行优化。) - Bergi
@JonMarkPerry 是的,但在99.99999%的情况下应该避免使用eval() - 当然包括这个。 - Scott Marcus
显示剩余2条评论
2个回答

20

你是正确的,词法作用域是这种行为的原因。当计时器函数运行时(这将在当前运行的代码完成后进行),它们会尝试解析 i 并查找作用域链。由于词法作用域,i 仅在作用域链中存在一次(比计时器函数高一个作用域),此时,i6,因为此时循环已经结束。

var 关键字使 JavaScript 中的变量具有函数或全局作用域(取决于声明位置)。在你的代码中,var i 使得 i 变量存在于全局作用域中(因为你的代码不在函数内),每个计时器函数必须解析同一个 i,直到它们最终运行。由于计时器函数要等到循环完成才会运行,所以 i 的值是循环导致它的最后一个值 (6)。

var i 改为 let i 以创建块级作用域 从而解决这个问题。

let 为变量创建块级作用域。在每次循环迭代时,你都会进入循环块并为 i 创建一个单独的作用域,每个计时器函数都可以使用它。

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


因此,我已完全停止使用var。对于大多数C语法风格的编程语言来说,函数作用域变量并不是常态。正如答案所述,let强制执行块级作用域。 - simon
1
let 也不会被提升,用于参考原始帖子。使用 var 时会有很多内部问题,请小心。 - simon
谢谢。我知道解决方案。我只是想澄清一下i的值是如何解析的。它是通过词法作用域解析的吗?当回调函数被触发时,它们是否通过词法作用域查找i的值。由于回调函数在外部作用域中具有闭包变量i,所以i的值是否被词法解析。这意味着回调函数将首先在其范围内查找该值,然后在外部范围内查找该值?谢谢。 - Hugo Perea
@HugoPerea 是的,JavaScript 使用词法作用域。闭包就是这个的副产品。 - Scott Marcus
1
“up'ed”这个词可能是我读过的最简洁、最清晰的第一段,而且未经编辑,很容易适用于这个永无止境的同样问题的各种变化。 - radarbob
显示剩余11条评论

5

让我用您的代码来解释:

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中的“块级作用域”,(var i = 1;i <=5 ;i++)会创建一个在整个函数中都存在并且可以被本地作用域或闭包作用域中的函数修改的变量。这就是为什么ECMAScript 6中有let的原因。
通过将var更改为let可轻松解决此问题。
for (let i = 1; i <= 5; i++) {
  setTimeout(function() { console.log(i); }, 1000*i);
}

谢谢。我知道这是如何工作的,我只需要帮助理解我所问的那部分。 - Hugo Perea

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