一道考察JavaScript闭包的经典面试题有点不懂

for (var i = 0; i < 10; ++i) {
    setTimeout(function () {console.log(i)}, 0);
}

请问为什么前面那个执行之后输出全都是10,后面那个就是0~9呢?

for (var i = 0; i < 10; ++i) {
    setTimeout((function () {console.log(i)})(), 0);
}

还有就是setTimeout的第二个参数为0该怎么理解?我百度了一下网上好像没人能够讲清楚这个啊(网上有人说setTimeout是异步执行,0的话会导致输出乱序,但是事实上后面那个代码段执行是0~9的递增数列,没有乱啊,这到底是怎么回事呢?)

阅读 6.1k
6 个回答
  1. 通过setTimeout添加的事件是是存在事件队列中的

  2. 当js线程有其他代码运行时,不会取事件队列中的函数运行

  3. ES5没有块级作用域

这就是说第一个for循环执行时,那些函数都添加到事件队列中了,当for运行完,开始取事件队列中的函数运行,但是在那些个函数中没有找到i,就会沿着作用域链向上查找,找到了i,但此时的i值是10,所以输出的全是10

第二个你的写法应该有错误,根本没有使用闭包,就是直接执行了函数

(function(i){
  return function(){
     console.log(i)
  }
})(i)

这里使用了立即执行函数,创建了一个函数作用域,在这个作用域中有i的值,9个立即执行函数作用域中的值的分别为0-9

回到上面取事件队列中的函数运行,沿作用域链查找时正好找到我们上面创建的立即执行函数作用域中有i,于是就输出了

关于闭包,我在这篇文章里有超级详细的解释,你的问题能快速被解决。

关于setTimeout的第二个参数为0,你首先要搞清楚,setTimeout的执行时间。

在js的函数调用栈中,所有的setTimeout会先进入一个自己的队列,该队列会在函数调用栈被清空之后才会依次开始执行。比如下面的例子

(function() {
    for (var i = 0; i < 100; ++i) {
        setTimeout(function () {console.log(i)}, 0);
    }  

    console.log('xxxxx');  
    console.log('xxxxx');  
    console.log('xxxxx');  
    for(var i = 0; i < 10000; ++i) {
        console.log('xxxxx');  
    }
})()

尽管后面有一万次循环需要花很多时间,但是setTiemout的队列仍然需要等待你们执行完毕才会开始执行,而这里的0,就是这些代码执行完毕之后0秒钟开始执行setTimeout的队列。

至于你的第一个例子,虽然也用到了闭包,但是由于循环的10个闭包中保存的都是同一个作用域中的i值,所以最后setTimeout执行时访问的i值是for循环最后的结果,就是10。

你的第二个例子,其实就和setTimeout没什么关系了,因为被立即执行,他的第一个参数就变成了一个数字。所以你的第二个例子虽然是依次输出了,但是就跟setTimeout没半点关系了。把0改成100,1000,仍然会立即执行输出。

所以根据上面的思路,正确的写法,是首先我们要把i值,保存在不同的作用域里,并形成闭包,写法如下

for (var i = 0; i < 10; ++i) {
    (function(i) {
        setTimeout(function () {console.log(i)}, i * 100);
    })(i)
}

// 延迟时间设置为i*100,可以依次执行,观察闭包效果更好。

第一段代码,在执行的时候,上可以理解成这样:

for (var i = 0; i < 10; ++i) {
    function the_no_name_function() { // 匿名函数和有名字的函数,本质上都是在当前作用域定义的
        console.log(i)
    }
    setTimeout(the_no_name_function, 0); // 所以你setTimeout里面定义的匿名函数可以看作是the_on_name_function
}

// setTimeout会在当前脚本的所有语句执行完一遍以后才会执行
// 理由很简单,js本质上是同步执行的,像setTimeout这样需要异步回调的话,放到所有语句执行后才执行,就不会阻塞正常的操作了

// 所以这个定义的匿名函数到在现在才被调用
// 这时候for语句已经运行完了,所以i已经是10了
the_no_name_function(); // i是10了
// ......
// 继续调the_no_name_function()九次,每次i都是10;

第二段代码,可以理解成:

for (var i = 0; i < 10; ++i) {
    (function the_no_name_function() {
        console.log(i)
    })(); // 函数在这次循环定义完之后,就立即执行了,立马输出本次循环的i值
    // 又由于你匿名函数没有返回东西
    setTimeout(undefined, 0); // 所以你传给setTimeout的实际上是undefined
}

// 这时候由setTimeout延迟到后面执行的,是10次undefined咯

哈哈,这是形象的理解,希望对你有帮助。

for (var i = 0; i < 10; ++i) {
    setTimeout(function () {console.log(i)}, 0);
}

上面这个在循环中setTimeout()将要执行的console.log(i)写入到了事件队列,也就是要等循环执行结束才依次执行,而循环执行结束后全局变量i已经自增加到10,所以会输出10个10,你可以在setTimeout()之前执行console.log(i)试一下,代码如下:

for (var i = 0; i < 10; ++i) {
    console.log(i);
    setTimeout(function () {console.log(i)}, 0);
}

输出结果如图:
clipboard.png

这样就能看出setTimeout()的执行顺序了


而第二个setTimeout()里面放的是一个闭包的自执行函数,也就是在循环的时候已经执行了console.log(i);所以输出的便是每次循环时的变量i;

for (var i = 0; i < 10; ++i) {
    setTimeout((function () {console.log(i)})(), 0);
}

也可以再setTimeout(i)前面加一段代码看一下执行顺序,如下:

for (var i = 0; i < 10; ++i) {
    console.log(i);
    setTimeout((function () {console.log(i)})(), 0);
}

执行结果如图:

clipboard.png

可以看到其实这样写console.log(i);setTimeout((function () {console.log(i)})(), 0);其实是在每次循环都执行掉的

第一个为什么全都是 10 楼上已经回答了.

for (var i = 0; i < 10; ++i) {
    setTimeout((function () {
        console.log(i)
    })(), 0);
}

这第二个, 注意看, setTimeout 里是一个立即执行的匿名函数,
setTimeout 是把这个立即执行的匿名函数的结果入队到任务队列.
console.log(i) 是立即执行的, 不会延时进入队列.相当于

for (var i = 0; i < 10; ++i) {
    console.log(i)
}

那么, 是什么延时进入到队列了呢? 是这个函数的 return 值. 这里没有 return 语句, 那么函数的默认返回结果就是 undefined , setTimeout 是把 undefined 延时放入到任务队列.
我把代码稍微改一下, 题主想想输出结果是什么

for (var i = 0; i < 10; ++i) {
    setTimeout((function () {
        return function(){
              console.log(i)
       }
    })(), 0);
}
新手上路,请多包涵

很简单就是要等主的程序执行完 for循环跑完 跑完之后i =10

推荐问题
宣传栏