在for循环中使用setTimeout无法连续输出值

322

我有这个脚本:

for (var i = 1; i <= 2; i++) {
    setTimeout(function() { alert(i) }, 100);
}

但是两次警报都会弹出3,而不是先弹出1然后再弹出2

有没有一种方法可以传递i,而不必将函数编写为字符串?


9
这里的所有答案都不起作用。它们每个都只是延迟了设定的时间,然后立即运行整个循环而没有进一步的延迟。从原帖的代码来看,显然他们想要在每次迭代中延迟。 - Chuck Le Butt
14
使用 "let" 关键字而非 var,这样可以解决此问题。 - Varadha31590
9
我曾经尝试过类似的事情,但没有人能够回答我的问题或解释我在概念上做错了什么。你需要理解的是:setTimeout()是异步的:JavaScript引擎不会等待n毫秒(在你的例子中为100毫秒)再继续执行。它只是做了一个“心理记录”:“在100毫秒后执行(在这种情况下是)Alert”,然后继续执行循环。它会在100毫秒内执行所有3次(或300次)迭代,因此最终,在时间到达之后,它会一次性输出所有3(或300)个警报。 - PakiPat
2
我认为你可以使用 let 而不是 var。这将解决你的问题。 - Mari Selvan
2
如何解决这个问题?你能澄清一下吗? - Tejas
显示剩余4条评论
10个回答

426
你需要为超时函数的每个实例安排一个不同的 "i" 副本。

function doSetTimeout(i) {
  setTimeout(function() {
    alert(i);
  }, 100);
}

for (var i = 1; i <= 2; ++i)
  doSetTimeout(i);

如果您不像这样做(还有其他类似的变体),那么每个计时器处理程序函数将共享同一个变量“i”。当循环完成时,“i”的值是什么?它是3!通过使用中介函数,变量的值的副本被创建。由于超时处理程序是在该副本的上下文中创建的,因此它具有自己的私有“i”可供使用。
编辑:
一些评论表明,设置几个超时会导致处理程序同时触发,这引起了一些混淆。重要的是要理解,设置计时器的过程——调用setTimeout()——几乎不需要时间。也就是说,告诉系统“请在1000毫秒后调用此函数”几乎会立即返回,因为将超时请求安装到计时器队列中的过程非常快。
因此,如果进行超时请求的连续操作,并且每个超时请求的时间延迟值相同,那么一旦经过了这段时间,所有计时器处理程序将依次快速调用。

If what you need is for the handlers to be called at intervals, you can either use setInterval(), which is called exactly like setTimeout() but which will fire more than once after repeated delays of the requested amount, or instead you can establish the timeouts and multiply the time value by your iteration counter. That is, to modify my example code:

function doScaledTimeout(i) {
 setTimeout(function() {
   alert(I);
 }, i * 5000);
}

(With a 100 millisecond timeout, the effect won't be very obvious, so I bumped the number up to 5000.) The value of i is multiplied by the base delay value, so calling that 5 times in a loop will result in delays of 5 seconds, 10 seconds, 15 seconds, 20 seconds, and 25 seconds.

更新

在2018年,有一个更简单的替代方案。通过在比函数更小的作用域中声明变量的新能力,如果进行如下修改,则原始代码将继续工作:

for (let i = 1; i <= 2; i++) {
  setTimeout(function() {
    alert(i)
  }, 100);
}

let 声明与 var 不同,它会为循环的每次迭代创建一个独立的 i


7
这是首选方法,因为它不会在循环体内创建函数定义。其他方法也能正常工作,但不如这个好(尽管它们展示了 JavaScript 的强大之处 ;) )。 - JAAulde
1
@JAAulde 我承认我个人会用匿名函数来实现,但这种方式作为一个例子更好。 - Pointy
1
我个人更喜欢匿名函数,因为我不想设置一堆名称。懒得想它们。 - Derek 朕會功夫
4
@Pointy :对我来说这不起作用,JavaScript 程序会等待 100 毫秒,然后整个 for 循环会立即执行。如果我做错了什么,请指出来。 - Parag Gangil
1
@sahilsolanki 不会,直到 for 循环完成后超时才会触发。即使超时为零秒,setTimeout() 调用也总是推迟函数的执行。 - Pointy
显示剩余15条评论

198

你可以使用立即执行函数表达式 (IIFE) 来创建一个setTimeout的闭包:

for (var i = 1; i <= 3; i++) {
    (function(index) {
        setTimeout(function() { alert(index); }, i * 1000);
    })(i);
}


2
这个不起作用:http://jsfiddle.net/Ljr9fq88/ - Chuck Le Butt
2
IIFE(Immediately Invoked Function Expression)实际上是一种无名称函数和立即执行的便捷方式-这就是“被接受的答案”实际上所做的,没有快捷方式-它会将函数调用封装在另一个函数中,因此内部函数会获得外部函数参数的本地副本。 - Karan Kaw
1
我的最短路径:在for循环中使用let而不是var - hien
2
您的方案是唯一一个对我有效的,我不想让所有请求一起运行,而是想要在等待一段时间后逐个执行每个事件。谢谢! - Edenshaw
1
同感,这是唯一一个对我有效的!谢谢! - PalmThreeStudio
显示剩余2条评论

43

这是因为的原因!

  1. 超时函数回调在循环完成后都能正常运行。实际上,就算每次迭代使用setTimeout(.., 0),所有这些函数回调仍然会严格在循环完成后运行,这就是为什么结果为3的原因。
  2. 这两个函数,虽然它们在每次循环迭代中被单独定义,但是它们闭包了同一个共享全局作用域,该作用域实际上只有一个i。

解决方案是通过使用一个自执行函数(匿名或更好的IIFE)为每次迭代声明一个单独的作用域,并在其中拥有i的副本,像这样:

for (var i = 1; i <= 2; i++) {

     (function(){

         var j = i;
         setTimeout(function() { console.log(j) }, 100);

     })();

}

更加清洁的那一个

for (var i = 1; i <= 2; i++) {

     (function(i){ 

         setTimeout(function() { console.log(i) }, 100);

     })(i);

}

在每次迭代中使用一个IIFE(自执行函数)创建了一个新的作用域,使得我们的超时函数回调有机会为每次迭代关闭一个新的作用域,其中包含了一个变量,该变量具有适当的每次迭代值,可以供我们访问。


当我将变量i设置为更大的数字(3或更大)时,警报的数字顺序变得奇怪。你能解释一下为什么吗?是因为setTimeout还是alert?非常感谢。 - Oboo Cheng
谢谢,因为您提到的问题,我为了演示的目的将 alert() 更改为 **console.log()**。 至少在Chrome中它工作正常!关于这个问题,请查看这个问题Question - Mehdi Raash
演示得非常精彩!! - Badal Saibo

27

setTimeout的函数参数正在捕获循环变量。在第一次超时之前,循环已经完成并显示了当前值i,即3

由于JavaScript变量只有函数作用域,解决方法是将循环变量传递给设置超时的函数。您可以像这样声明和调用这样的函数:

for (var i = 1; i <= 2; i++) {
    (function (x) {
        setTimeout(function () { alert(x); }, 100);
    })(i);
}

这个不起作用:http://jsfiddle.net/sq5n52xj/ - Chuck Le Butt
3
为使其工作,只需将延迟乘以i。像这样:setTimeout(function () { alert(x); }, i*100); - Placid
你只需要将 var 替换为 let 关键词,它就会打印数字 1 和 2。但是这里又有一个问题,它将仅在 2 秒后同时打印 1 和 2。 如果你想要每隔一秒钟打印一次 1 和 2,则在 setTimeout 回调中,将 1000 修改为 i * 1000 - Pushp Singh

23
你可以使用setTimeout的额外参数将参数传递给回调函数。

for (var i = 1; i <= 2; i++) {
        setTimeout(function(j) { alert(j) }, 100, i);
}

注意:此方法不适用于IE9及以下浏览器。

这里有一个针对IE问题的polyfill;https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout - Mark Schultheiss
真不错 :) 没想到 setTimeout 还接受第三个参数。 - Aamir Afridi

9

答案

我正在使用它来制作添加物品到购物车的动画-当点击产品的“添加”按钮时,购物车图标从页面上飘到购物车区域:

function addCartItem(opts) {
    for (var i=0; i<opts.qty; i++) {
        setTimeout(function() {
            console.log('ADDED ONE!');
        }, 1000*i);
    }
};

注意,持续时间以单位n的时间为基准。

因此,从单击时刻开始,每个动画的起始时间(epoc)是每秒单位乘以项目数量的乘积。

epoc: https://en.wikipedia.org/wiki/Epoch_(reference_date)

希望这能帮到您!


1
你也可以像这样将参数传递给回调函数:setTimeout(function(arg){...}, 1000*i, 'myArg'); - Cody
终于有人给出了正确的答案!运行得非常好。 - strix25

5
您可以使用bind方法。
for (var i = 1, j = 1; i <= 3; i++, j++) {
    setTimeout(function() {
        alert(this);
    }.bind(i), j * 100);
}

3
这是将数字赋值给this的设置。但我明白了。类似于这个,您也可以使用setTimeout(console.log.bind(console,i), 1000); - Muhammad Umer
1
setTimeout(console.log, 1000, i); 这个也同样有效。 - Sarath Babu Nuthimadugu

2

好的,基于Cody的答案,另一个可行的解决方案是这样的:

但更加通用的方法可能是这样的:

function timedAlert(msg, timing){
    setTimeout(function(){
        alert(msg);    
    }, timing);
}

function yourFunction(time, counter){
    for (var i = 1; i <= counter; i++) {
        var msg = i, timing = i * time * 1000; //this is in seconds
        timedAlert (msg, timing);
    };
}

yourFunction(timeInSeconds, counter); // well here are the values of your choice.

0

我曾经遇到过同样的问题,以下是我的解决方法。

假设我想要12个2秒的延迟

    function animate(i){
         myVar=setTimeout(function(){
            alert(i);
            if(i==12){
              clearTimeout(myVar);
              return;
            }
           animate(i+1)
         },2000)
    }

    var i=1; //i is the start point 1 to 12 that is
    animate(i); //1,2,3,4..12 will be alerted with 2 sec delay

animate(i); - 如果你调用 animate(1),这个方法才能按照描述正常工作,因为 i 是未定义的。使用其他值将无法正确运行。该参数在最好的情况下是毫无意义的。 - nathanchere
这段代码应该会创建12个连续的延迟。这里的“i”是用来改变延迟数量的,如果是1,那么就会有12个延迟;如果是11,那么就只会有2个延迟。 - Raj Nandan Sharma

-21

真正的解决方案在这里,但你需要熟悉PHP编程语言。你必须混合使用PHP和JAVASCRIPT命令才能达到你的目的。

请注意:

<?php 
for($i=1;$i<=3;$i++){
echo "<script language='javascript' >
setTimeout(function(){alert('".$i."');},3000);  
</script>";
}
?> 

它正好做你想要的事情,但要小心PHP变量和JAVASCRIPT变量之间的关系如何建立。


1
这个方法是通过为每次调用创建一个单独的计时器来实现的,但它们都会同时触发,所以…… - Muhammad Umer
1
不再是插值...它可以混合全新的两个宇宙。 - shekhardtu

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