JavaScript中的setTimeout与循环(Loop)

6
我有一个非常琐碎的问题。像这样的简单循环使用setTimeout:
(原文已经是英语,此处为直译)
for (var count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);
}

控制台会输出如下内容:
Count = 3
Count = 3
Count = 3

不确定为什么输出是这样的。请问有人能解释一下吗?
9个回答

6

这与JavaScript中作用域和变量提升的处理方式有关。

在您的代码中发生的情况是,JS引擎会将您的代码修改为:

var count;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);
}

setTimeout() 被执行时,它首先查找自己的作用域内是否有 count ,但没有找到,然后会开始查找关闭函数(这种情况被称为闭包)中的 setTimeout 函数,直到找到 var count 声明,该声明将具有值3,因为在第一个超时函数执行之前循环已经完成。 更加详细的代码说明如下:
//first iteration
var count = 0; //this is 1 because of count++ in your for loop.

for (count = 0; count < 3; count++) { 
    setTimeout(function() {
        alert("Count = " + 1);
    }, 1000 * 1);
}
count = count + 1; //count = 1

//second iteration
var count = 1;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + 2);
    }, 1000 * 2);
}
count = count + 1; //count = 2

//third iteration
var count = 2;

for (count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + 3);
    }, 1000 * 3);
}
count = count + 1; //count = 3

//after 1000 ms
window.setTimeout(alert(count));
//after 2000 ms
window.setTimeout(alert(count));
//after 3000 ms
window.setTimeout(alert(count));

4

这样想:

在1000*n毫秒结束后,count的值将是多少?

当然会是3,因为for循环比1000*n毫秒的超时时间早结束了。

为了打印出1、2、3,你需要以下代码:

for (var count = 0; count < 3; count++) {
    do_alert(num);
}

function do_alert(num) {
    setTimeout(function() {
        alert("Count = " + num);
    }, 1000 * num);
}

另一种方法是将其制作成一个“闭包函数”(在JavaScript closures vs. anonymous functions中有很好的解释)。
for (var count = 0; count < 3; count++) {
    (function(num){setTimeout(function() {
        alert("Count = " + num);
    }, 1000 * num)})(count);
}

这两个代码段的作用实际上是相似的。

第一个样例在每次迭代中调用一个命名函数(do_alert)。

第二个样例在每次迭代中调用一个闭包匿名函数(与do_alert类似)。

这完全取决于作用域。

希望这可以帮到你。


1

解决方法很简单,就是使用 ES6 的 let 关键字来定义局部变量。你的代码几乎不用修改,但它会按照你的期望运行 :)

 for (let count = 0; count < 3; count++) {
    setTimeout(function() {
        alert("Count = " + count);
    }, 1000 * count);

}

或者你可以创建一个递归函数来完成这项工作,如下所示:

function timedAlert(n) {
  if (n < 3) {
    setTimeout(function() {
        alert("Count = " + n);
        timedAlert(++n);
    }, 1000);
  }
}

timedAlert(0);

稍微解释一下,这是因为如果你创建一个 var 变量,循环会在每次迭代中使用同一个变量,但是当使用 let 时,它会在每次迭代中创建一个全新的变量。 - Vincent

1
这与闭包作用域有关。对于每个setTimeout回调函数,相同的变量count在其作用域中都是可用的。您正在增加它的值并创建一个函数,但是每个函数实例都具有相同的变量count在其作用域中,并且在回调函数执行时,它将具有值3。
您需要在for循环中内部创建变量的副本(例如var localCount = count),以使其正常工作。由于for不会创建作用域(这就是整个问题的原因),因此您需要使用函数作用域引入一个作用域。
例如:
for (var i = 0; i < 5; i++) {
  (function() {
    var j = i;
    setTimeout(function() {
      console.log(j)
    },
    j*100);
   })();
 }

不起作用:for (var i = 0; i < 5; i++) { var j = i; setTimeout(function(){ console.log(j) }, j*100); } - rofrol
这是因为'j'变量被提升到父级作用域中,因为for循环没有创建它。你需要创建一个新的函数作用域来隔离j: for (var i = 0; i < 5; i++) { (function() { var j = i; setTimeout(function(){ console.log(j) }, j*100); })()} - Joe
那么为什么要创建副本,当你可以使用IIFE并将变量传递给它 (function(j) {})(i) 呢? - rofrol
是的,你也可以这样做,而且可能更简单。有不同的方法来做事情。我只是修改了一个旧答案来回应你的观点。 - Joe

1

想一想:

  1. 代码执行一个循环,在循环中它设置一些代码稍后运行。
  2. 循环结束。
  3. setTimeout代码执行。此时count的值将是多少?循环已经结束很久了...

1

首先,setTimeout(function, milliseconds) 是一个函数,它接受一个要在 "milliseconds" 毫秒后执行的函数。

记住,JS将函数视为对象,因此for(...)循环最初会产生类似以下内容的东西:

setTimeout( ... ) setTimeout( ... ) setTimeout( ... )

现在,setTimeout()函数将一个一个地执行。

setTimeout()函数将尝试在当前作用域中查找count变量。如果失败,则会转到外部作用域并找到count,其值已经通过for循环递增到3。

现在,开始执行...第一个警报立即显示,因为毫秒为0,第二个警报在1000毫秒后显示,然后第三个警报在2000毫秒后显示。所有这些都显示Count = 3


0

更好的解决方案是“忘记循环和递归”在这种情况下,使用包含“setTimeOut”的“setInterval”组合:

    function iAsk(lvl){
        var i=0;
        var intr =setInterval(function(){ // start the loop 
            i++; // increment it
            if(i>lvl){ // check if the end round reached.
                clearInterval(intr);
                return;
            }
            setTimeout(function(){
                $(".imag").prop("src",pPng); // do first bla bla bla after 50 millisecond
            },50);
            setTimeout(function(){
                 // do another bla bla bla after 100 millisecond.
                seq[i-1]=(Math.ceil(Math.random()*4)).toString();
                $("#hh").after('<br>'+i + ' : rand= '+(Math.ceil(Math.random()*4)).toString()+' > '+seq[i-1]);
                $("#d"+seq[i-1]).prop("src",pGif);
                var d =document.getElementById('aud');
                d.play();                   
            },100);
            setTimeout(function(){
                // keep adding bla bla bla till you done :)
                $("#d"+seq[i-1]).prop("src",pPng);
            },900);
        },1000); // loop waiting time must be >= 900 (biggest timeOut for inside actions)
    }

附注:请理解(setTimeOut)的真实行为:它们都将在同一时间开始,“三个倒计时将同时开始”,因此请设置不同的超时时间以安排执行。

附注2:这是一个定时循环的示例,但对于反应循环,您可以使用事件、Promise、async/await等。


0

这是因为当 for 循环完成执行时,计数器已经变成了 3,然后才会调用 set timeout。

试试这个:

var count = 0; 
setTimeout(function() {
       for (count = 0; count < 3; count++) {
           alert("Count = " + count);
        }
}, 1000* count);

0

这是因为所有的超时函数都在循环结束后运行。

然后,超时函数会获取计数器的当前值。

而那总是3,因为for循环已经结束了。


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