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

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

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

同步优先、异步靠边、回调垫底!

公式表达:同步=>异步=>回调

这口诀的用处是什么呢?至少应付面试,完全够用!

例1:(经典面试题)

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

想必大家都遇到过这样的题目吧,那么为什么会是这样的输出结果呢?

来,跟着我念:”同步优先,异步靠边,回调垫底!“

首先:for循环及循环外部的console是同步的,所以先执行for循环再执行外部的console.log-->同步优先

再来看:同步代码应该是顺序执行,为什么先输出的是”5“呢?

原因:for循环是先执行,但是setTimeout的回调函数是不能接收到参数的(回调垫底),等for循环执行完,就会执行外部的console.log了,

所以先打印的会是外部console-->5

继续:外部console执行完之后为什么会是输出了5个”i:5“呢?

这里就涉及到JavaScript的执行栈和消息队列的概念了,

概念的详细解释可以看下阮老师的JavaScript运行机制详解:再谈Event Loop-阮一峰的网路日志,或者看并发模型与Event Loop。

我拿这个例子做一下讲解,JavaScript单线程如何处理回调呢?JS同步的代码是在堆栈中顺序执行的,而setTimeout回调会先被放到消息队列,

for循环每执行一次就会放一个setTimeout到消息队列排队等候,同步代码执行完了,再去顺序执行消息队列上的回调方法。

这个例子中,也就是说先执行for循环,按顺序放置了5个setTimeout回调到消息队列,然后for循环结束后,再执行其他的同步代码也就是外部的console

,至此,堆栈中已经没有同步的代码了,就去消息队列中讯息好,发现了5个setTimeout(也是根据之前放置的顺序而执行的)

到这里:已经知道了为什么setTimeout是最后执行的了

那么:为什么是5个5呢?

JavaScript在把setTimeout放到消息队列的过程中,循环的i是不会及时保存进去的,相当于你谢了一个异步方法,但是ajax的结果都还没能返回,只能等到返回之后才能传参到异步函数中,也就是同步代码都还没执行完,i是不会被传入到回调函数的。

在这里,因为i是用var定义的,所以是全局变量(因为此处没有其他的函数,如果有其他的函数,那i就是此函数内部变量),当for循环执行完毕,i值为5,从外部的console也可看出,那么当同步函数执行完毕,回调接收到的i也就是5了(很多人都会以为setTimeout里面的i会是for循环过程中的i值,这种理解是不正确的)

例2:我们在例1中加上一行代码。

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循环,循环里面的console是同步的,所以先输出,结束后再执行外部的console,最后再执行setTimeout回调函数!

是不是so easy?

那么面试官如果再问,如何解决这个问题?

很简单,当然是ES6中的最新特性!let!!!

例3:

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

咦~为什么外部的console.log(i)会报错呢?

你这个口诀是不是哪里不对劲呢?

let是ES6的语法(ECMAScript 6,JavaScript最新规范,主流浏览器已基本支持该规范,并持续向该规范靠拢,其中,IE比较特殊想必大家都知道的!

在PC端开发的时候,要注意IE9以下的兼容,移动端开发时,可以比较放心了!

目前实际项目中,安全的做法是运用babel工具将ES6解释为ES5)

ES5中的变量作用域是函数,而let语法的作用域是当前块,这里就是for循环体了。

我们来分析一下,用了let作为变量i的定义之后,作用与代码块中,此处也就是指for循环,for循环每执行一次,都会先给循环内的setTimeout传参数i,每次传入的参数依次是0,1,2,3,4,每接受一次参数然后循环内的setTimeout被放到消息队列(带入了传入的参数i,与之前var定义的i是不同的),for循环执行完毕,i不在作用在当前块之外的代码中,所以外部的同步代码console输出的i为定义!当同步代码执行完毕,再依次取出消息队列中带有不同参数的回调函数,所有输出的结果是如上所示!

在这里let本质上就是形成了一个闭包。也就是下面例4这种写法一样的意思,如果面试官说用下面例4的方式,你可以正儿八经的告诉他:这就是一个意思!

这也就是为什么有人说let是语法糖!

例:4:

var loop = function(_i){

setTimeout(function(){

console.log('2:',_i);

},1000);

}

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

loop(i)

}

console.log(_i);

//输出

5

2:0

2:1

2:2

2:3

2:4

或许这或让面试官联想到闭包问题,什么是闭包呢?耐心往下看,后面讲。

回到ES5,你是不是就发现适合我的口诀了?同步优先=>异步靠边=>回调垫底!

而用let的时候。你看不懂?你需要真正的了解ES6的语法原理!

注意!

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

也就是说loop(_1)是外部函数,setTimeout是内部函数,当setTimeout被loop的变量访问的时候,就形成了一个闭包!

例5:

function test(){

var a=10;

var b = function(){

console.log(a);

}

b();

}

test();//输出10

口诀继续:同步优先=>异步靠边=>回调垫底

先执行函数test,然后JS进入test函数内部,定义了一个变量,然后执行函数b,进入函数b内部,打印a,这里都是同步代码,那么这里怎么解释闭包?

解释:函数test是外部函数,函数b是内部函数,当函数b被外部函数test的变量访问的时候,就形成了闭包。

回归正题!

上面主要讲了同步、异步、回调的执行顺序问题,接着我就举一个简单的同步、异步、回调的例子

例6:

let a=new Promise(

function(){

console.log(1);

setTimeout(()=>consoel.log(2),0);

console.log(3);

console.log(4);

resolve(true);

}

);

a.then(v=>{

console.log(8)

});

let b=new Promise(

function(){

console.log(5);

setTimeout(()=>console.log(6),0);

}

);

console.log(7);

一眼看不出名堂,不过不慌!

先读口诀:同步优先=>异步靠边=>回调垫底

1、看同步代码:a变量是一个Promise,我们知道Promise是异步的,是指他的then()和catch()方法,Promise本身还是同步的,所以这里先执行a变量内部的Promise同步代码(同步优先)

2、Promise内部有4个console,第二个是一个setTimeout回调(回调垫底)。所以这里先输出1,3,4,回调的方法当然是丢到消息队列中排队等着。

3、接着执行resolve(true),进入then(),then是异步,下面还有同步代码没执行,所以也被丢到消息队列中排队等待(异步靠边)

4、b变量也是一个Promise,和a一样,先执行内部的同步代码,输出5,setTimeout滚如消息队列排队等待。

5、最下面同步输出7

6、同步代码执行完了,Javascript就跑到消息队列呼叫异步的代码:这里的异步只有then,输出8

7、异步也执行完了,接着就是去消息队列中依次找到回调了:这里2个回调在排队,setTimeout时间参数都是0,不做任何影响,只是跟他们在消息队列中的排队顺序有关,所以先输出a里面的2,最后输出b里面的回调6

8、最终输出结果是:1、3、4、5、7、8、2、6

PS:如果想变得有趣一点的话,我们可以稍微做一点点修改,把a里面Promise的setTimeout的时间蚕食0改成2,也就是2ms后执行,为什么不是1ms,1ms的话,浏览器都还没有反应过来呢。改成>=2的数字才能看到2个setTimeout的输出顺序发生了变化。所以回调函数正常情况下是在消息队列中顺序执行的,但是使用setTimeout的时候,还需要注意时间大小也会改变它的顺序(感觉上是改变了顺序,其实先读检索的消息队列上的回调还是a,不过因为时间参数的原因,被滞后2ms执行了)。

口诀不一定是万能的,不过作为一种辅助,更重要的还是要理解JavaScript的运行机制,才能对代码的执行顺序有清晰的路线。

还有async/await等其他异步的方案,不管是那种异步,基本都试用于这个口诀,对于新手来说,可以快速读懂面试官出的js笔试题目,做出快速准确并且也是面试官希望得到的答案,以后再也不用怕做到类似的笔试题目啦!

PS:特殊情况下不适应口诀也很正常!JavaScript博大精深,不是一句话就能概括出来的,随着ES6的推广与开拓,JavaScript一定会有更为快速的发展!但是万变不离其宗,掌握JavaScript的底层机制始终异常重要!

最后:再念一次口诀!同步优先=>异步靠边=>回调垫底!

(原文自前端教程=>hyy1115)

你可能感兴趣的:(JavaScript)