1

使用setTimeout替代setInterval

setInterval()这个间歇调用函数是应用得比较广的,尤其在比较古老的浏览器中实现动画效果时,往往离不开它。然而这个函数却有不少坑,由于其实现是把要执行的代码插入待执行队列排队执行,同时为防止连续执行,这个队列中只能有一个最早进来的它的代码实例。如果队列中也有其他任务在等待,而且执行了很长时间,首先就很容易导致计时不准;再者,还会打乱其执行的时间线,导致setInterval()迟迟不能再添加新的代码实例,最终出现略过某次执行的现象等问题,如下图所示:

85ad0d9cjw1f1bmifthorj20q40bpt9p.jpg

如果你要获取每次间歇执行的结果,那就要避免setInterval()可能略过某次执行缺点,可以用setTimeout()改写它:

    //原函数
    var intervalId = setInterval(function () {
        if(someCondition) {
            clearInterval(intervalId);
            console.log('done');
            }
        }, 1000);

    //改写后
    function fun() {
        if(someCondition) {
                console.log('done');
            } else 
            {
                setTimeout(fun, 1000);
            }
    }

    setTimeout(fun, 1000);

虽然setTimeout()的计时也未必很准确,但由于上述代码是链式使用的,一环扣一环,从源头阻止了略过某次执行的情况。同时使用setTimeout()也更灵活些、定制性更强,想执行到某个周期就停下来,只要你不继续调用就可以了,没必要专门再加个clearTimeout()。最后,既然是链式执行,那你第一个调用可用普通的函数调用即可,然后在执行时自然会调用setTimeout(),而不用像setInterval()那样第一个调用也必须等待一段时间,这样可以满足一些更细致的要求。

如何给setTimeout()的回调函数传参?

这个问题其实也代表了一类问题,即如何在如setTimeout()setInterval()、指定DOM事件等限制使用函数引用而非函数调用的场合,仍然能给将来要调用的函数传递参数。

为什么要给函数传参?因为传参和返回值一样,是函数作为子程序与外界通信的一大手段。但在上面提到那些限制必须使用函数引用的场景下,如果我们只用函数的引用而没有调用函数,那就意味着不能给直接给它传参、也不能接受它的返回值,这就使得子程序间的数据通信少了一大功能,此时,你要让回调函数与外界通信貌似也就只有通过全局变量了。

但深入想一想,这些场景只是规定要用到函数引用,并没有说一定是回调函数的引用,那也意味着我们可以偷梁换柱,用别的函数引用来“占着”本来应该是回调函数引用的位置,而让回调函数进行调用。于是我们就有了解决方法,以setTimeout()为例最简单的一种想法如下:

    setTimeout(function() {
        callback(arg);
    }, timeout);

这里用到一个匿名函数作为第一个参数,匿名函数提供了它的函数引用后,回调函数就可以在匿名函数体内直接调用、传参了。而写成return callback(arg)还可以满足你获得回调函数返回值的需求。

除了外套匿名函数这种方法,我们还可以使用功能更加强大的闭包,来解决这个传参问题:

    setTimeout((function(arg) {
        return function () {
            callback(arg);
        };
    })(arg), timeout);

其实就是在第一种方法的基础上再外套一个立即执行表达式,这个闭包实际上是包含了传参操作的,但有了它我们就可以来解决那些我们通过构造闭包可以解决的的问题了。比如获取某次循环等更具体的块级作用域中的变量值:

for (i in Arr) {
    setTimeout((function(arg) {
        return function () {
            callback(arg);
        };
    })(i), timeout);
}

函数声明和函数表达式

在使用立即执行表达式(IIFE)时,我们常采用(function () {})()这种形式,若去掉第一组括号直接在函数声明后面加括号则会报错:

    function () {}() //Uncaught SyntaxError: Unexpected token (

显然js的函数声明后面是不能直接加括号让它立即执行的,但我们把函数声明括起来,立即执行的就不是一个函数,而是一个所谓函数表达式了。而构建函数表达式的方法也不止把声明括起来这种,一些其他的操作符也可以,比如赋值号:

    var A = function () {}();

到目前为止,我们似乎能够得出结论:函数声明后不可直接跟圆括号,而函数表达式后面可以。然而js认为什么是函数声明、什么是函数表达式却并不是简单靠函数声明的外在形式来决定的,比如下面这个例子:

    function A(i){
        console.log(i);
    }

    A(function () {
        return 5;
    }()); //5

上面的代码里A函数的参数直接在函数声明后面加上了括号,看起来是个非法的IIFE了,而这个“非法”IIFE作为参数时却能正常地立即执行。但再仔细想想,我们知道传参的过程其实是相当于赋值的,所以这种传参形式其实和上面提到的通过赋值号构建函数表达式应该是一个道理,立即执行的其实还是是函数表达式,而非函数声明,不要被表像迷惑了。

而为防止以后读代码时再踩坑,我想出了个更简单的区分两者的“方法”:只要function不是放在一个语句最前面时就可以连同后面的声明内容当作函数表达式,就可以加括号立即执行。

当然在写代码用到IIFE时,就没必要纠结这些降低效率了,直接在声明外括括号得了,这样既写起来方便又看起来简洁明了。


Levon
526 声望89 粉丝

大自然爱好者