JavaScript - for循环内部的setTimeout函数导致代码在for循环完成之前执行

3

我有一个for循环,需要延迟每次重复以进行动画。如果我删除setTimeOut函数,下面的代码可以正常循环,变量正确地递增通过循环,然后执行底部代码行。但是使用setTimeOut函数后,底部代码会先执行,然后for循环执行7次(应该是6次),并在每次告诉我x = 6。显然我做错了什么。有什么想法吗?

for ( x = 0; x <= 5; x++) {
    setTimeout(function() {
        alert("For loop iteration #" + x);
    }, 500 * x);
}
alert("Code to be executed after completed for loop");

你的意思是(应该是6)。顺便说一下,这个问题每两天就会被问一次,让我来搜索一下... - kapa
好的,我会编辑。 - americanknight
例如:http://stackoverflow.com/questions/8567118/javascript-settimeout-issue-w-for-loop?rq=1 http://stackoverflow.com/questions/13774004/all-the-settimeouts-inside-javascript-for-loop-happen-at-once - kapa
3个回答

5

您需要使用闭包来保存当前x值在闭包上下文中。

 for (var x = 0; x <= 5; x++) {
    (function(x) {
        setTimeout(function(){
            alert("For loop iteration #" + x);
            if (x == 5) {
                setTimeout(function(){
                    alert("Code to be executed after completed for loop");
                });
            }
        }, 5 * x);

    })(x);
}

谢谢。这解决了循环内警报的问题,但是循环后面的代码仍然先执行,而我需要它在循环完成后再执行。 - americanknight
尝试使用我回答中的新版本。 - Silver_Clash
为什么要使用没有延迟参数的setTimeout而不是普通调用? - Christoph
这是一种使用“零”延迟设置回调的方法之一。http://www.w3.org/html/wg/drafts/html/master/webappapis.html#timers 最常见的问题是,据我所知,setTimeout目前在ecma脚本规范中并没有正式定义。 - Silver_Clash

3

这是一个常见的概念误区。

  1. Javascript是非阻塞的
  2. 传递的是变量的引用,而不是实际值

需要记住的是,变量 x 是动态的。传递给 alert("For loop iteration #" + x); 的是对 x 的引用,而不是值。因此,当 alert 最终被执行时,x 将具有在执行点而不是 setTimeout 启动点处的值!

基本上就是这样:
您的循环被处理,创建6个超时,并立即显示您的 alert("Code to be executed after completed for loop");。然后经过一段时间,你的超时被执行,然后会显示变量x在循环完成后的状态-6

您需要一个闭包,以便将变量 x 的值传递给警报,而不是变量 x 本身的引用。

for (var x = 0; x <= 5; x++) {
    (function(z) {
        setTimeout(function() {
            alert("For loop iteration #" + z);
        }, 5 * z);
    })(x);
}

编辑:

要解决你的第二个问题,你需要使用回调函数。回调函数是你代码的逻辑延续,但不应立即执行,而需要推迟到某个特定点(即最后一个警报出现)。你可以这样实现它:

for (var x = 0; x <= 5; x++) {
    (function(z) {
        setTimeout(function() {
            alert("For loop iteration #" + z);
            if (z===5){ continue_code() }
        }, 5 * z);
    })(x);
}

function continue_code(){
    alert("Code to be executed after completed for loop");
    // Here comes all your code
    // which has to wait for the timeouts from your for loop
}

在最后一个setTimeout中,你调用了一个函数来继续执行你的代码。

在Javascript中,变量不是通过引用传递的!它是通过共享传递给函数的! - Silver_Clash
@Silver_Clash,我没有谈论参数如何在函数之间传递,而是关于函数作用域内的表达式。它是一个传递给 alert 的引用,而不是实际值。这通常被称为按引用传递而不是按值传递。 - Christoph
警告只需使用调用它的上下文中的变量对象。如果我们不使用闭包,那么警告将使用与for循环相同的上下文(警告仅使用变量,并且变量未在setTimeout函数上下文中声明,它从作用域链获取变量,没有传递变量!)在这种情况下谈论传递变量是没有意义的,因为我们不改变上下文。 - Silver_Clash
好了解,谢谢。你上面的代码修复了循环中警报的问题,但是循环后面的代码仍然先执行,而我需要它在循环完成之前不执行。 - americanknight
@user1543227 我已经编辑了我的答案来解决这个问题。如果一个人以前没有使用过异步编程范式,那么可能需要进一步阅读回调函数的相关知识,因为这个主题可能在开始时有点难以理解。 - Christoph
显示剩余2条评论

1

x是一个全局变量。在第一个警报出现时,您已将其增加到6。如果您不希望发生这种情况,请改用类似以下方式,在每500毫秒调用的函数内进行递增:

var x = 0;
var interval = setInterval(function() {
    alert("Loop iteration #" + x++);
    if(x==5) {
        clearInterval(interval);
        alert("Code to be executed after completing loop");
    }
}, 500);

为什么值得贬低这个?我意识到我将 forsetTimeout 改成了 setInterval,但这只是一个建议,而且同样有效。 - Sygmoral
我没有点踩,但可能的解释是为什么它被踩了。1)变量是全局的还是不全局的并不重要。2)虽然结果仍然相同,但您的代码与OP的代码完全不同。3)您创建了额外的全局(=邪恶)变量,而一个简单的闭包就可以解决问题。 - Christoph
这似乎运行良好,只是对我来说最后一行仍然先执行,而我需要它最后执行。 - americanknight
我忽略了最后一行应该被称为“last”。已经修复了。至于其他评论:感谢反馈,但我不完全同意其中的所有内容,因为我认为在这种情况下使用间隔比超时和闭包更合适 :) (我承认我假设代码片段位于其他函数内部,给变量一个有限的作用域) - Sygmoral
这绝对是最简单的解决方案。谢谢。 - americanknight
@user1543227 我不明白这里为什么更容易。你必须将所有应在超时后运行的代码放入 setInterval 中,如果你有多个 alert 需要执行,这将完全搞乱你的代码。 - Christoph

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