使用setTimeout()调用函数

50

简单地说...

为什么会...

setTimeout('playNote('+currentaudio.id+', '+noteTime+')', delay);

工作得很完美,在指定延迟后调用函数,但是

setTimeout(playNote(currentaudio.id,noteTime), delay);

同时调用playNote函数吗?

(这些setTimeout()在一个for循环中)

或者,如果我的解释太难理解了,这两个函数有什么区别?


2
不要在循环中使用setTimeout,而应该使用setInterval()。它会在延迟间隔上一遍又一遍地调用您指定的函数,直到您告诉它停止为止。 - Cfreak
在一行文本前面添加4个空格可以将其格式化为代码。选择一个代码块,按下 ctrl-k 即可实现此操作。 - Peter Ajtai
请注意@Cfreak提供的建议...除非必要,不要手动循环。引擎有处理重复事件的方法,您不必自己重新发明它(如果做错了,您可能会导致可爱的“此脚本已停止响应”)。 - jcolebrand
1
哈哈,谢谢大家,但我必须使用setTimeout(),因为延迟每次都不同,而不是恒定的 :) - quantum rookie
6个回答

79

你列出的第一个形式有效,因为它将在delay末尾计算一个字符串。通常不建议使用eval(),因此应避免使用它。

第二种方法不起作用,因为你立即使用函数调用运算符()执行了函数对象。使用形式playNote(...)时,playNote将立即执行,因此在延迟结束时什么也不会发生。

相反,你必须将匿名函数传递给setTimeout,因此正确的形式是:

setTimeout(function() { playNote(currentaudio.id,noteTime) }, delay);

请注意,您正在传递 setTimeout 整个函数表达式,因此它将保留匿名函数并仅在延迟结束时执行它。

您也可以传递 setTimeout 引用,因为引用没有立即执行,但这样您就无法传递参数:

setTimeout(playNote, delay);

注意:

对于重复的事件,您可以使用setInterval(),并且您可以将 setInterval() 赋值给一个变量,并使用该变量使用clearInterval()停止时间间隔。

如果您在 for 循环中使用 setTimeout(),则在许多情况下,最好使用递归函数来代替。这是因为在 for 循环中,setTimeout() 中使用的变量将不会是当 setTimeout() 开始时的变量,而是在延迟后函数被触发时的变量。

只需使用递归函数就可以避开整个问题。

使用递归处理可变延迟时间:

  // Set original delay
var delay = 500;

  // Call the function for the first time, to begin the recursion.
playNote(xxx, yyy);

  // The recursive function
function playNote(theId, theTime)
{
    // Do whatever has to be done
    // ...

    // Have the function call itself again after a delay, if necessary
    //   you can modify the arguments that you use here. As an
    //   example I add 20 to theTime each time. You can also modify
    //   the delay. I add 1/2 a second to the delay each time as an example.
    //   You can use a condition to continue or stop the recursion

    delay += 500;

    if (condition)
    { setTimeout(function() { playNote(theID, theTime + 20) }, delay); }
}

3
严格来说,这并不是递归,因为该函数没有直接调用自身,而只是将另一个对自身的调用排队以供稍后执行。关键在于,在启动下一个调用之前,每个调用都会返回。 - Doin
3
你的“递归”代码存在一个大问题,由于闭包工作的方式,每次对“playNote”的连续调用都会向闭包链中添加一个条目,其长度将无限增加。像无限递归一样,这是个坏主意 - 最终你会耗尽内存!我已经编辑了答案,展示如何避免这种情况,同时保留该方法的一般性。 - Doin

8

试试这个。

setTimeout(function() { playNote(currentaudio.id,noteTime) }, delay);

7
不要使用字符串超时。这实际上是一种"eval",是一个不好的事情。 它能够工作是因为它将 "currentaudio.id" 和 "noteTime" 转换为它们自己的字符串表示,并隐藏在代码中。只有当这些值具有生成JavaScript文字语法的toString()时,才能正常工作,这对于Number是正确的,但对于其他大多数情况则不是。
setTimeout(playNote(currentaudio.id, noteTime), delay);

那是一个函数调用。立即调用了 playNote 函数,并将该函数的返回结果(可能是 undefined)传递给了 setTimeout(),而不是你想要的内容。
正如其他答案所提到的,你可以使用内联函数表达式和闭包来引用 currentaudionoteTime
setTimeout(function() {
    playNote(currentaudio.id, noteTime);
}, delay);

然而,如果您处于循环中,并且每次循环currentaudionoteTime不同,那么您将遇到闭包循环问题:相同的变量将在每个超时引用,因此当它们被调用时,每次都会得到相同的值,即在早期循环完成时留在变量中的值。
您可以通过另一个闭包来解决这个问题,为循环的每次迭代复制变量的值:
setTimeout(function() {
    return function(currentaudio, noteTime) {
        playNote(currentaudio.id, noteTime);
    };
}(currentaudio, noteTime), delay);

但现在这个情况有点丑陋。更好的选择是使用Function#bind,它可以为您部分应用一个函数:

setTimeout(playNote.bind(window, currentaudio.id, noteTime), delay);

(window用于在函数内设置this的值,这是bind()的一个特性,在此不需要使用。)

然而,这是ECMAScript第五版的功能,不是所有浏览器都支持。因此,如果您想使用它,您必须先进行支持的hack,例如:

// Make ECMA262-5 Function#bind work on older browsers
//
if (!('bind' in Function.prototype)) {
    Function.prototype.bind= function(owner) {
        var that= this;
        if (arguments.length<=1) {
            return function() {
                return that.apply(owner, arguments);
            };
        } else {
            var args= Array.prototype.slice.call(arguments, 1);
            return function() {
                return that.apply(owner, arguments.length===0? args : args.concat(Array.prototype.slice.call(arguments)));
            };
        }
    };
}

6
我专门注册了这个网站的账户来评论Peter Ajtai的答案(目前最高票),但是发现你需要50个声望才能评论,所以我将其作为答案。他的回答中提到了以下几点:

You can also pass setTimeout a reference, since a reference isn't executed immediately, but then you can't pass arguments:

setTimeout(playNote, delay);

这是不正确的。在给setTimeout函数传递函数引用和延迟时间后,任何附加参数都会被解析为引用函数的参数。下面的示例比将函数调用封装在函数中更好。

setTimeout(playNote, delay, currentaudio.id, noteTime)

始终查阅文档。

话虽如此,正如Peter所指出的,如果您想要在每个playNote()之间变化延迟,递归函数是一个好主意,或者考虑使用setInterval()如果您希望每个playNote()之间有相同的延迟。

还值得注意的是,如果您想将for循环的i解析为setTimeout(),您需要将其包装在函数中,详见此处


3

了解JavaScript执行代码的时机以及等待执行某些内容可能会有所帮助:

let foo2 = function foo(bar=baz()){ console.log(bar); return bar()}

  • JavaScript首先执行函数构造器,创建一个函数对象。您可以使用函数关键字语法=>语法,您会得到类似的(但不完全相同)结果。
  • 刚创建的函数然后被分配给变量foo2
  • 此时还没有运行任何其他函数(既没有调用baz也没有调用bar,没有查找任何值等)。但是,函数内部的语法已经被检查过了。
  • 如果将foofoo2传递给setTimeout,则在超时后,它将调用该函数,就像您执行foo()一样。(请注意,不会传递任何参数给foo。这是因为setTimeout默认不传递参数,虽然它可以, 但是这些参数在超时到期之前被计算,而不是在超时到期时被计算)
  • 调用foo后,将评估默认参数。由于我们调用foo而没有传递参数,因此将评估bar的默认值。(如果我们传递了一个参数,则不会发生这种情况)
  • 在评估bar的默认参数时,JavaScript首先查找名为baz的变量。如果找到一个变量,则尝试将其作为函数调用。如果成功,则将返回值保存到bar中。
  • 现在评估函数的主体:
  • JavaScript查找变量bar,然后使用结果调用console.log。这不会调用bar。但是,如果它被称为bar(),那么bar将首先运行,然后将bar()的返回值传递给console.log。请注意,在调用函数之前,甚至在查找函数以查看其是否存在且确实是函数之前,JavaScript获取要调用的函数的参数值。
  • JavaScript再次查找bar,然后尝试将其作为函数调用。如果成功,则该值将作为foo()的结果返回
因此,函数体和默认参数不会立即调用,但其他所有内容都会。同样地,如果您进行函数调用(即()),那么该函数也将立即执行。但是,您不需要调用函数。省略括号将允许您传递该函数并稍后调用它。然而,这样做的缺点是,您无法指定要调用函数时要使用的参数。另外,JavaScript 在调用函数或查找存储函数的变量之前会在函数括号内执行所有操作。

2
因为您的第二个参数是在调用 playNote 函数之前将其传递给 setTimeout 函数。

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