最近在js中使用定时器的时候,遇到了一些让人困惑的问题,后来经过了查阅资料和测试,对定时器和闭包函数的使用多了一点点的了解。在这里作小小记录。

定时器中的setTimeout()

setTimeout() :在指定的毫秒数后调用函数或计算表达式。
语法: myVar= window.setTimeout("javascript function", milliseconds);
其中,第一个参数是含有 JavaScript 语句的字符串。第二个参数指示从当前起多少毫秒后执行第一个参数。

闭包函数

w3cschool: 闭包是可访问上一层函数作用域里变量的函数,即便上一层函数已经关闭。
www.ruanyifeng.com: 闭包就是能够读取其他函数内部变量的函数。

它们的结合

有一个这样的测试页面。(*代表某个testTimeout函数的代号,下面会说明)

<!DOCTYPE html>
<html lang="zh-cn">
<head>
  <title>测试setTimeout</title>
</head>
<body onload="testTimeout*()">
  <script>
    function testLog(j) {
      for (var i = 0; i <= j; i++) {
        console.log('循环中: ' + i);
      }
      console.log('\n\n');
    }

    /*
    为了简化版面和便于说明,这里省略几个testTimeout*()函数,放到下面展示。
    */

  </script>
</body>
</html>

这个页面中有一段js代码,其中定义了几个函数。首先是一个用于打印从0到i的函数testLog(i是调用它时传过来的参数)。
然后是一系列的testTimeout函数。这些testTimeout函数的初衷都是希望能够循环调用testLog函数,每次调用都相隔同样的一段时间,并且在控制台输出从0到当前循环次数i一共i+1个整数。但是,结果不尽相同。下面是对它们的分析记录。


首先是testTimeout1

    function testTimeout1() {
      console.log('testTimeout1 is running: ');
      for (var i = 0; i < 5; i++) {
        setTimeout(testLog(i), i * 1000);
      }
    }

testTimeout1测试结果:每个定时器的i中传送给testLog的参数i都是当前循环次数,就是这个循环索引是正确被记录了,第一次打印0,第二次打印0 1…这样。但是定时器的时间间隔是不是有误?都相等且几乎都为0,没有等待1秒,因为全部都一下子同时打印出来了。
原因:其实不是因为定时器有误,而是因为setTimeout的第一个参数接受的是js代码字符串或者函数名,如果写了一个函数名还给它传一个参数的话,就相当于马上执行了这个函数,没有真正把需要执行的代码传递给定时器。所有给计时器传的函数因为带了参数,所以都是马上执行的,所以就有一种计时器的时间不对的错觉。


testTimeout2

    function testTimeout2() {
      console.log('testTimeout2 is running: ');
      for (var i = 0; i < 5; i++) {
        setTimeout(function () { testLog(i) }, i * 1000);
      }
    }

这个测试函数与第一个的不同:同样是每一次循环设置一个时长为i*1000的定时器,但是用一个匿名函数包住testLog(i)传给setTimeout。
测试结果:每个定时器传给testLog的参数 i 都相等,都是4,所以5次批量输出都输出0 1 2 3 4
但是定时器的时间间隔是正确的,做到了每秒打印一批0到i的效果,只不过每次调用testLog都打印出来0 1 2 3 4 ,没有把正确的循环索引记录下来传递过去
原因:用匿名函数的话可以做到把要执行的函数作为参数传递给定时器。但是,在这里,由于这个匿名函数内没有变量i,所以它要往上一级寻找变量i,而当它找到i的时候,这个i已经是循环结束时的i,也就是4了。
所以每个定时器尽管计时正确,但其实都只用最终循环次数i作为参数。


testTimeout3

    function testTimeout3() {
      console.log('testTimeout3 is running: ');
      for (var i = 0; i < 5; i++) {
        (function (i) {
          setTimeout(function () { testLog(i) }, i * 1000);
        })(i)
      }
    }

这个测试函数的特点:在上一个testTimeout2的基础上,在每个循环中,用一个类似(function(i){...})(i)这样的匿名函数包住定时器。这是一个可以自我执行的闭包函数,传给它的参数正是当前的循环索引i。
测试结果:按照既定的等待时间倍数,把从0开始到正确的循环次数i分批打印出来了。符合预期。
原因:这里的for循环中,每一次循环中都有一个自执行的匿名函数,它大概长(function(i){...})(i)这样。有了这个匿名函数,当次循环的次数i就能传进去,而它里面的计时器中的匿名函数由于自身没有变量i而往外部寻找时,找到的正是这次循环中自我执行的匿名函数的参数i,这个i正是这次循环一开始就传递进来的循环次数i,所以是正确的。计时器要执行的代码参数正确,匿名函数的参数i正确,所以就没问题了。


testTimeout4

    function testTimeout4() {
      console.log('testTimeout3 is running: ');
      for (var i = 0; i < 5; i++) {
        var a = function (i) {
          setTimeout(function () { testLog(i) }, i * 1000);
        }(i);
      }
    }

testTimeout3()的基础上,去掉了for循环闭包函数的小括号,并且把这个函数赋给一个变量。
测试结果:和testTimeout3()一样正常。要注意的是不将这个闭包函数赋给一个变量的话是会出错的。可参考JS中for循环里面的闭包问题的原因及解决办法


下面来自阮一峰博客中关于闭包的一个例子

    var name = "The Window";
    var object = {
      name: "My Object",
      name2: "name2",
      getNameFunc: function () {
        return function () {
          return this.name;
        };
      }
    };

    alert(object.getNameFunc()());
    alert(object.name2);

关于为什么会提示The window,根据一个评论(轩脉刃)所说。

  1. 函数中的this指的是调用这个函数的owner
  2. object.getNameFunc()是返回一个函数,并没有执行函数中的代码

因此,在这个例子里,个人觉得,alert()实际上是在用(function(){})()这种形式执行一个闭包函数。先看前一部分,object.getNameFunc(),这时候应该是将闭包函数返回,然后相当于是(函数)()这样的形式,就是全局对象this调用一个闭包函数,所以才会显示’The window’。