JavaScript同步、异步、回调执行顺序之经典闭包setTimeout面试题分析

同步、异步、回调?傻傻分不清楚。

大家注意了,教大家一道口诀:

同步优先、异步靠边、回调垫底(读起来不顺)

用公式表达就是:

同步 => 异步 => 回调

这口诀有什么用呢?用来对付面试的。

有一道经典的面试题:

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

console.log(i);

//输出
5
i:  5
i:  5
i:  5
i:  5
i:  5

这道题目大家都遇到过了吧,那么为什么会输出这个呢?记住我们的口诀 同步 => 异步 => 回调

1、for循环和循环体外部的console是同步的,所以先执行for循环,再执行外部的console.log。(同步优先)

2、for循环里面有一个setTimeout回调,他是垫底的存在,只能最后执行。(回调垫底)

那么,为什么我们最先输出的是5呢?

非常好理解,for循环先执行,但是不会给setTimeout传参(回调垫底),等for循环执行完,就会给setTimeout传参,而外部的console打印出5是因为for循环执行完成了。

知乎有大神讲解过 80% 应聘者都不及格的 JS 面试题 ,就是以这个例子为开头的。但是没有说为什么setTimeout是输出5个5。

这里涉及到JavaScript执行栈和消息队列的概念,概念的详细解释可以看阮老师的 JavaScript 运行机制详解:再谈Event Loop - 阮一峰的网络日志,或者看 并发模型与Event Loop

《图片来自于MDN官方》

我拿这个例子做一下讲解,JavaScript单线程如何处理回调呢?JavaScript同步的代码是在堆栈中顺序执行的,而setTimeout回调会先放到消息队列,for循环每执行一次,就会放一个setTimeout到消息队列排队等候,当同步的代码执行完了,再去调用消息队列的回调方法。

在这个经典例子中,也就是说,先执行for循环,按顺序放了5个setTimeout回调到消息队列,然后for循环结束,下面还有一个同步的console,执行完console之后,堆栈中已经没有同步的代码了,就去消息队列找,发现找到了5个setTimeout,注意setTimeout是有顺序的。

那么,setTimeout既然在最后才执行,那么他输出的i又是什么呢?答案就是5。。有人说不是废话吗?

现在告诉大家为什么setTimeout全都是5,JavaScript在把setTimeout放到消息队列的过程中,循环的i是不会及时保存进去的,相当于你写了一个异步的方法,但是ajax的结果还没返回,只能等到返回之后才能传参到异步函数中。
在这里也是一样,for循环结束之后,因为i是用var定义的,所以var是全局变量(这里没有函数,如果有就是函数内部的变量),这个时候的i是5,从外部的console输出结果就可以知道。那么当执行setTimeout的时候,由于全局变量的i已经是5了,所以传入setTimeout中的每个参数都是5。很多人都会以为setTimeout里面的i是for循环过程中的i,这种理解是不对的。

===========================================分割线=========================================

看了上面的解释,你是不是有点头晕,没事,继续深入讲解。

我们给第一个例子加一行代码。

for (var i = 0; i < 5; ++i) {
    setTimeout(function() {
        console.log('2: ',i);
    }, 1000);
    console.log('1: ', i); //新加一行代码
}

console.log(i);

//输出
1:  0
1:  1
1:  2
1:  3
1:  4
5
2:  5
2:  5
2:  5
2:  5
2:  5

来,大家再跟着我一起念一遍:同步 => 异步 => 回调 (强化记忆)

这个例子可以很清楚的看到先执行for循环,for循环里面的console是同步的,所以先输出,for循环结束后,执行外部的console输出5,最后再执行setTimeout回调 55555。。。

=====================================分割线============================================

这么简单,不够带劲是不是,那么面试官会问,怎么解决这个问题?

最简单的当然是let语法啦。。

for (let i = 0; i < 5; ++i) {
    setTimeout(function() {
        console.log('2: ',i);
    }, 1000);
}

console.log(i);

//输出
i is not defined
2:  0
2:  1
2:  2
2:  3
2:  4

咦,有同学问,为什么外部的i报错了呢?
又有同学问,你这个口诀在这里好像不适应啊?

let是ES6语法,ES5中的变量作用域是函数,而let语法的作用域是当前块,在这里就是for循环体。在这里,let本质上就是形成了一个闭包。也就是下面这种写法一样的意思。如果面试官对你说用下面的这种方式,还有let的方式,你可以严肃的告诉他:这就是一个意思!这也就是为什么有人说let是语法糖。

var loop = function (_i) {
    setTimeout(function() {
        console.log('2:', _i);
    }, 1000);
};

for (var _i = 0; _i < 5; _i++) {
    loop(_i);
}

console.log(i);

面试官总说闭包、闭包、闭包,什么是闭包?后面再讲。

写成ES5的形式,你是不是发现就适合我说的口诀了?而用let的时候,你发现看不懂?那是因为你没有真正了解ES6的语法原理。

我们来分析一下,用了let作为变量i的定义之后,for循环每执行一次,都会先给setTimeout传参,准确的说是给loop传参,loop形成了一个闭包,这样就执行了5个loop,每个loop传的参数分别是0,1,2,3,4,然后loop里面的setTimeout会进入消息队列排队等候。当外部的console执行完毕,因为for循环里的i变成了一个新的变量 _i ,所以在外部的console.log(i)是不存在的。

现在可以解释闭包的概念了:当内部函数以某一种方式被任何一个外部函数作用域访问时,一个闭包就产生了。

我知道你又要我解释这句话了,loop(_i)是外部函数,setTimeout是内部函数,当setTimeout被loop的变量访问的时候,就形成了一个闭包。(别说你又晕了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值