当前位置: 首页 > Web前端 > CSS

JavaScript系列—JavaScript同步、异步、回调执行时序分析经典闭包setTimeout面试题

时间:2023-03-30 18:12:01 CSS

同步、异步、回调?傻傻分不清。大家注意,我教大家一个公式:先同步,异步到边,回调到底(读起来不好)。公式为:同步=>异步=>回调。这个公式有什么用?用于采访。有个经典的面试题:for(vari=0;i<5;i++){setTimeout(function(){console.log('i:',i);},1000);}console.log(i);//输出5i:5i:5i:5i:5i:5这个问题大家都遇到过,那为什么是这个输出呢?记住我们的口头禅synchronous=>asynchronous=>callback1,for循环和循环体外的console是同步的,所以先执行for循环,再执行外部console.log。(同步优先)2.for循环中有一个setTimeout回调,在最底层,只能最后执行。(底部回调)那么,为什么我们先输出5呢?很容易理解,for循环先执行,但是不会给setTimeout传递参数(回调在最下面)。for循环执行完后,会传参给setTimeout,外部控制台打印5,因为for循环执行完毕。知道有高手讲解80%申请者都失败的JS面试题,就从这个例子入手。但是没有说为什么setTimeout输出55。这涉及到JavaScript执行栈和消息队列的概念。概念的详细解释可以参考阮老师的JavaScript运行机制详解:浅谈EventLoop-阮一峰的博客,或者看并发模型和EventLoop《图片来自于MDN官方》我就拿这个举例说明吧,JavaScript单线程如何处理回调?JavaScript同步代码在栈中顺序执行,setTimeout回调会先放入消息队列。每次执行for循环,都会在消息队列中放入一个setTimeout排队等待。当同步代码执行时,会调用消息队列的回调方法。在这个经典的例子中,也就是说先执行for循环,在消息队列中依次放入5个setTimeout回调,然后for循环结束,下面有一个同步控制台。console执行完后,在stack里面没有同步代码找到,于是去消息队列找,发现有5个setTimeouts。请注意,setTimeouts是有序的。那么,既然最后执行了setTimeout,那么他输出的i是什么呢?答案是5..有人说这不是废话?现在告诉你为什么setTimeout全是5。当JavaScript把setTimeout放入消息队列时,循环i不会及时保存。相当于写了一个异步方法,但是ajax结果还没有返回。参数只有在返回后才能传递给异步函数。这里也是一样。for循环结束后,因为i是用var定义的,var是一个全局变量(这里没有函数,如果有就是函数内部的变量)。此时i为5,从外部控制台输出结果即可知道。那么在执行setTimeout的时候,由于全局变量的i已经是5,所以传入setTimeout的每个参数都是5。很多人认为setTimeout中的i就是for循环过程中的i,这种理解是错误的。======================================================================================================================================================================================================================================================================================================继续深入讲解。让我们在第一个示例中添加一行代码。for(vari=0;i<5;++i){setTimeout(function(){console.log('2:',i);},1000);console.log('1:',i);//新增一行代码}console.log(i);//Output1:01:11:21:31:452:52:52:52:52:5来,大家跟我再读一遍:Synchronization=>asynchronous=>callback(强化记忆)在这个例子中可以清楚的看到for循环先执行,for循环中console是同步的,所以先输出,for循环结束后,执行外部控制台输出5,最后ExecutesetTimeoutcallback55555。。.=========================================分割线=================================================这么简单,还不够精彩吗?然后面试官会问这个问题怎么解决?最简单的当然是let语法。.for(leti=0;i<5;++i){setTimeout(function(){console.log('2:',i);},1000);}console.log(i);//输出iisnotdefined2:02:12:22:32:4哎,有同学问,为什么外部i报错了?另一个同学问,你的公式好像不适合这里?let是ES6语法,ES5中的变量作用域是函数,let语法的作用域是当前块,这里是for循环体。在这里,let本质上形成了一个闭包。这和下面的写法是同一个意思。如果面试官告诉你使用下面的方法和let方法,你可以认真的告诉他:就是这个意思!这也是为什么有人说let是语法糖的原因。varloop=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会进入message排队等候。当执行外部控制台时,因为for循环中的i变成了一个新的变量_i,所以外部console.log(i)不存在。现在可以解释闭包的概念:当任何外部函数范围以某种方式访问??内部函数时,就会创建一个闭包。我知道你想让我再解释一遍这句话。loop(_i)是外部函数,setTimeout是内部函数。当循环变量访问setTimeout时,形成一个闭包。(别跟我说你又晕了?)举个新例子吧。函数t(){变量a=10;varb=function(){console.log(a);}b();}t();//输出10和我念咒语:synchronous=>asynchronous=>Callback(强化记忆)先执行函数t,然后js进入t,定义一个变量,然后执行函数b,进入b,然后打印a,这里都是同步代码,没有异议,所以这里怎么解释Closure:函数t是外部函数,函数b是内部函数。当函数b被函数t的变量访问时,就形成了一个闭包。=============================================分割线================================================================================================================================================================,======================,===============================,上面提到的执行顺序下面我举个例子,包括同步,异步,回调。leta=newPromise(function(resolve,reject){console.log(1)setTimeout(()=>console.log(2),0)console.log(3)console.log(4)resolve(true)})a.then(v=>{console.log(8)})letb=newPromise(function(){console.log(5)setTimeout(()=>console.log(6),0)})console.log(7)看到这个例子,不怕吗?先看公式:Synchronization=>Asynchronous=>Callback(加强记忆)1.看同步代码:变量a是一个Promise,我们知道Promise是Asynchronous是指它的then()和catch()方法。Promise本身还是同步的,所以这里先执行a变量里面的Promise同步代码。(同步优先级)console.log(1)setTimeout(()=>console.log(2),0)//回调console.log(3)console.log(4)2、Promise内部有4个console,第一个第二个是setTimeout回调(回调在底部)。所以这里输出1、3、4回调的方法被丢进消息队列排队等待。3、然后执行resolve(true),进入then(),then是异步的,下面还有同步要执行,所以then也滚到消息队列排队等待。(真可怜)(asynchronousstepaside)4.变量b也是一个Promise。像a,执行内部同步代码,输出5,setTimeout滚到消息队列排队等待。5、底部同步输出7。6、同步代码执行完后,JavaScript去消息队列调用异步代码:asynchronous,出来执行。那么就只有一个异步了,所以输出8.7,异步也结束了,轮到callback的children了:callback,出来执行。这里队列中有2个回调,他们的时间设置为0,所以不受时间影响,只和排队顺序有关。然后先在a输出回调2,最后在b输出回调6。8.最终输出的结果是:1,3,4,5,7,8,2,6。我们也可以稍微修改一下,更改setTimeout(()=>console.log(2),0)的在a中承诺到setTimeout(()=>console.log(2),2),是的,把时间改成了2ms,为什么不试试改成1呢?如果是1ms,浏览器还没有响应。如果改成大于等于2的数字,可以看到两个setTimeout的输出顺序发生了变化。所以回调函数在消息队列中正常是顺序执行的,但是在使用setTimeout时,也需要注意时间的大小,改变其顺序。========================================分割线===============================================================================================================================================================,理解JavaScript的运行机制,才能对代码执行的顺序有一个清晰的路线更为重要。还有其他异步解决方案,例如async/await。不管是哪种异步,这个公式基本都适用。对于新手来说,可以快速理解面试官给的js笔试题。您不必再害怕做笔试题了。人在特殊情况下不适应配方是正常的。JavaScript博大精深,不是一句话可以概括的。最后,跟着我读一遍口头禅:同步=>异步=>回调如果文章对你有帮助,请点击推荐。