这里面底层发生了什么?循环中的Javascript定时器

3

有人能简单地解释一下这里发生了什么吗?

function timerCheck() {
    for(var i=0; i<5; i++) {
        setTimeout(function() {
            console.log("Hello" + i);
        }, 3000);
    }
}

可能有些人已经知道,调用该函数不会按预期工作。实际上,每次都会同时调用该函数5次,并且每次i的值都设置为5。3秒后将输出以下内容:

Hello5
Hello5
Hello5
Hello5
Hello5

我知道使用setInterval方法是解决这种问题的正确方法,但我很好奇在幕后发生了什么。我真的想了解JavaScript的工作原理。请注意,我没有计算机科学背景,只是一个自学的编码者。


前一个问题要求解决方案。我正在寻找对实际情况的全面解释。因此,这不是那个问题的重复。 - Shaan
3个回答

2

这可能有助于更好地理解发生了什么:

function timerCheck() {
    for(var i=0; i<5; i++) {
        console.log("Hi" + i);
        setTimeout(function() {
            console.log("Hello" + i);
        }, 3000);
        console.log("Bye" + i);
    }
}

您会看到

Hi0
Bye0
Hi1
Bye1
Hi2
Bye2
Hi3
Bye3
Hi4
Bye4

立即打印到控制台,因为循环的所有五次迭代都非常快地完成,然后在五秒钟后您将看到:

Hello5
Hello5
Hello5
Hello5
Hello5

因为超时时间(所有的超时时间几乎同时设置)都在一起发生,而且由于循环已经完成:i == 5。这是由于i的作用域引起的。变量i在timerCheck()中声明后,其范围在任何地方都可以使用。在setTimeout设置的匿名函数中没有局部i,也没有var i,并且i没有作为参数传递给函数。您可以通过闭包轻松解决这个问题,这将返回一个具有i的本地副本的函数:
function timerCheck() {
    for(var i=0; i<5; i++) {
        setTimeout((function(loc_i) {
            return function() {
                console.log("Hello" + loc_i);
            };
        })(i), 3000);
    }
}

这将输出:

Hello0
Hello1
Hello2
Hello3
Hello4

为了理解这个问题:
(function(loc_i) {
    return function() {
        console.log("Hello" + loc_i);
    };
})(i)

你需要知道,在Javascript中,函数可以立即执行。例如:(function(x){ console.log(x); })('Hi');会在控制台打印出Hi。因此,上述外部函数只是接受一个参数(i的当前值),并将其存储到该函数的本地变量loc_i中。该函数立即返回一个新函数,该函数将"Hello" + loc_i打印到控制台。这就是传递给超时函数的函数。
希望这一切都讲得很清楚,如果您还有疑问,请告诉我。

好的,让我看看我是否理解了。循环运行非常快,并在几乎相同的时间内调用setTimeout函数5次(因为循环运行得如此之快)。因此,在等待setTimeout函数执行时,i已经达到了5。但是,您可以通过每次设置一个闭包来本地存储i,但这仍然会同时运行,对吗? - Shaan
@Shaan,是的,我认为你理解了。尝试将 console.log 更改为:console.log("Local: " + loc_i + ", i: " + i);。在第二个版本中,您会发现 loc_ii 都在内部函数中定义。 - Paul
是的,我现在看到了。loc_i指向闭包环境,而i指向函数计时器范围内设置的i。谢谢! - Shaan

1

JavaScript中的变量作用域仅限于函数。

在您的示例中,变量i声明在timerCheck内部。这意味着在循环结束时,i将等于5

现在,添加调用setTimeout并不改变i作用域为timerCheck的事实,并且i已经被修改为5,因此每个setTimeout调用内部的代码运行时都是如此。

您可以创建一个函数来“捕获”i的值,以便在从循环内部调用它时,您可以为setTimeout调用获取新的变量作用域:

function createTimer(j) {
    setTimeout(function() {
        console.log("Hello" + j);
    }, 3000);
}

function timerCheck() {
    for(var i=0; i<5; i++) {
        createTimer(i);
    }
}

由于createTimer接受一个参数j,当你从timerCheck中的for循环内部传递icreateTimer时,j现在被限定在createTimer中,以便每个setTimeout调用都有自己的j


0

这实际上是对安德鲁答案的补充。

如果您尝试设置一个设置输出的变量,它也会解释作用域。

function test()
{
    for(var i=0; i<5; i++) {
    t = "Hello " + i + "<br/>";
    document.write(t);
        setTimeout(function() {
            document.write(t);
        }, 3000);
    }
}

正如你所看到的,写入操作将按预期执行,但当setTimeout被触发时,t变量将是最后设置的,也就是Hello 4。

因此输出结果将是:

Hello 0
Hello 1
Hello 2
Hello 3
Hello 4

从循环中返回

Hello 4
Hello 4
Hello 4
Hello 4
Hello 4

来自setTimeout


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