【本周面试题】第2周 - js单线程和异步相关问题
硬性知识点考察:
为什么js是单线程的?
因为js设计最初是为了操作dom而生,如果是多线程的,当多个线程同时修改一个dom时就会产生冲突,所以设计成单线程,一次只能做一件事。
既然是单线程为什么要有异步?
还是因为js要和用户进行交互,对于一些耗时比较长的任务或者需要用户操作的事件任务,如果不使用异步,就会造成程序阻塞,那样用户不操作页面就永远卡死在某一处了。所以需要异步单独处理这一类任务。
js怎么实现异步的?
通过event loop事件循环实现,耗时任务由浏览器的web apis异步线程执行,条件成立触发回调,同步的任务和异步的回调任务通过事件循环被执行。
2018.11.19 周一面试题:js单线程导致的异步任务
考题:
var i = 5;
while(i--){
setTimeout(function(){
console.log(i)
},0)
}
console.log(666)
考点:
- while循环语句、隐式类型转换
- i - - 和 - - i 的区别
- 同步和任务队列
- 异步事件之定时器线程
答案:先执行 666 ,然后输出5个 -1
解析考点:
while(){}
就是一个循环语句,判断小括号内的条件,成立则执行大括号内的语句,否则结束循环。
就是0和非0会被隐式转换为true or false
隐形考点,while小括号内部,会进行隐式转换,将其他类型的值转为Boolean布尔值类型的进行判断
劝你别去试while(-1){console.log("I'm fantastic!")}。因为我刚才就是导致浏览器死机,然后关掉重写的博文【多么痛的领悟】
--i 和 i--
var i = 5;
while(--i){
setTimeout(function(){
console.log(i)
},0)
}
console.log(666)
那么结果就是这样了,输出4个0。
这是因为呢,
【i--】 这里的顺序,是先输出i,再执行i=i -1;
【--i】只是先做i=i -1;然后输出i;
也就是说,while(i--)的时候,是先判断i是否满足条件,然后再执行增减。
而while(--i)的时候,是先让i减一再去判断是否满足条件。
所以第一种情况中,i为0的时候被判断不满足条件,不再执行setTimeout,但是后边还是要再-1的,所以最后i变成了-1。
而第二种情况,当i等于1的时候,先让1-1,然后在判断0不满足条件,不再执行i为1时的条件语句,所以循环只有4次,且i最终值也为0。
setTimeout定时器导致的 异步任务
js是单线程的,而浏览器是多线程的。setTimeout属于定时器触发线程中的一种。
因为不能保证任务能够立马执行,所以他被扔到异步队列中,等待回调函数召唤,到时间后再执行。
等时机成熟,回调函数会被安排到主队列中排序入栈被执行。注意这里不要被定时器的时间迷惑了。
理论上执行的时间并不一定就是代码执行时定时器设定的延迟时间之后的时间。因为如果定时器到时,
主任务队列上还轮不着他说话的时候,他也不能插嘴。
同步和任务队列
因为遇到循环里的异步定时器事件,定时器内部的任务被扔到任务队列,等待同步任务执行完毕后执行。
所以循环完毕后,外边的console.log(666)按照同步的顺序先被执行并打印出来。而不是循环内部的console先起作用
2018.11.20 周二面试题:主任务队列上的同步任务与异步回调
考题:
1 setTimeout(function () {
2 console.log(1);
3 }, 0);
4 function foo() {
5 console.log(2);
6 }
7 function bar() {
8 console.log(3);
9 }
10 bar();
11 setTimeout(function () {
12 console.log(4);
13 }, 1000);
14 foo();
15 setTimeout(function () {
16 console.log(5);
17 }, 0);
考点:
同步任务
异步回调函数
定时器
解析:
答案是:3,2,1,5 约1秒后, 4
主要还是定时器的考点吧。不要被代码的书写顺序所疑惑。
首先,定时器出发前,所有的定时器任务都是当做异步任务被扔到异步“等待池”的。
当定时器触发时,异步的等待任务才会以回调函数的形式加入主队列中,然后根据定时器的时间来排回调函数的具体执行顺序。
所以,上边的代码中,虽然console(1) 按照代码顺序在上边。
但是优先处理的同步任务是bar(),其次是far()。也就是会先console(3)、console(2)执行
等这些同步任务全部执行完毕清空栈空间后,异步定时器任务才会按序执行(事件点击和请求除外)。
而按照定时器的顺序,两个定时器延迟时间为0的先执行,因为console(1)在上边,所以1比5先输出。
2018.11.21 周三面试题:连等赋值问题、运算符优先级
题目代码:
1 var a = {n:1};
2 var b = a;
3 a.x = a = {n:2};
4 console.log(a.x)
5 console.log(b.x)
考点:
- js堆栈空间
- 引用值拷贝(浅拷贝)
- 连等赋值的顺序(从右向左)
- “.”运算符赋值的优先级(高于等号赋值的顺序)
- 对象的属性不存在时,直接获取得到undefined。变量不存在时直接获取会报错:变量名 is not defined
答案与解析:
首先要知道的是,在js中,引用类型的值,在栈中存了个地址,指向堆内存中的真正位置和数据。
而引用类型的值再拷贝,新的变量拿到的还是这个地址,并在栈内存中开辟一块土地放置这个地址,同样指向源值在堆中的位置。
举个例子,
变量小明,公安局给他制作一个身份证,身份证上的地址是北京。
儿子变量小小明,公安局制作一个新的身份证,但是身份证上的地址也还是北京。具体位置不变。
套到上例,变量a,存放在栈内存中。其值是一个指针,指向堆内存中的 {n:1}。
var b = a;
先执行var b;然后b = a;
所以b在栈内存也开辟了一个新空间。然后新空间放一个内容就是和a的内容一致的、指向堆内存{n:1}的指针。
所以,b此时等于{n:1};
好,接下来重头戏来了:
a.x = a = {n:2}
首先要知道,js中变量连等赋值是从右向左的。
那我们就会理解为先是a = {n:2},然后a.x = {n:2}。
这样理解就掉坑里了。
这道题中一个关键的考点是有一个 . 运算符。这个点运算符的优先级高于 = 赋值运算符。
所以这一句连等的正确执行顺序应该是
a.x = {n:2}; // 此时b = a = {n:1,x: {n: 2}};获取b.x就是对象{n:2}
a = {n : 2}; // 此时整个a被重新赋值,a在栈内存中的指针指向了{n:2}这个对象,所以里边没了x属性,console.log(a.x)不报错,拿到的是undefined值。
此时的堆栈空间关系如图:
2018.11.22 周四面试题:同步任务、异步任务、宏任务与微任务的优先级
考题:
setTimeout(() => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(()=> {
console.log(3)
})
console.log(2)
考点:
js单线程的运行机制
任务队列
宏任务和微任务的优先级
考点知识点总结见:【本周主题】第一期:JavaScript单线程与异步
解析:
这里涉及到两个比较:
1、同步任务和异步任务的比较、优先级及执行顺序
2、宏任务和微任务的比较、优先级及执行顺序
不懂异步的我可能觉得答案是: 4、2、1、3
不懂微任务的我可能觉得答案是:2、4、1、3
懂异步不懂微任务的我可能觉得答案是:2、1、4、3
但实际答案应该是:1、2、3、4
下面分析一波:
因为setTimeout属于异步任务需要被挂起,所以引擎扫描到他的时候不会执行他,而是将其抛到web APIs里边当做异步任务处理,紧接着向下执行。
遇到new Promise会立即执行,因为这是构造函数创建实例,属于函数调用那一类的代码(new Promise()可以想象成是Promise()函数调用)。所以会读取promise内部代码,读到resolve(),因为resolve()是resolve形参函数的回调,依旧加入异步让其去排任务队列。
紧接着读取下边的console1这一行同步代码,打印出1
因为then需要resolve()调用执行时才会触发,所以也会跳过,
紧接着读到最后一行console2,同步代码,打印出2.
此时,执行栈清空,主线程空闲。就去任务队列里找回调函数来执行,
按照目前分析,此时任务队列里按照顺序加入了如下两个回调,且他们的顺序也应该如下图:
但是实际上,我们忽略了,任务队列中其实是双轨车道:
所以,两个回调函数1号和2号,虽然1号比2号早加入任务队列,但他们在里边的顺序是这样的
而主线程呢,又比较偏向微任务。所以执行宏任务之前,他会先去微任务那里问问,你有没有任务让我执行啊?
此时,resolve()准备就绪,所以主线程把他接走到执行栈执行,打印结果 3
然后主线程又问,你还有任务要我执行吗?
微任务回答:没了。
主线程就说,那好,宏任务轮到你了。
然后最后执行setTimeout的代码,打印4。
2018.11.23 周五面试题:宏任务和微任务的优先级
天啊,每天搜刮合适的面试题都好难啊。请回答下面代码中输出的顺序是什么?
1 Promise.resolve().then(()=>{
2 console.log('Promise1')
3 setTimeout(()=>{
4 console.log('setTimeout2')
5 },0)
6 })
7
8 setTimeout(()=>{
9 console.log('setTimeout1')
10 Promise.resolve().then(()=>{
11 console.log('Promise2')
12 })
13 },0)
我果断的做错了。。。
按照我的理解,顺序是这样的:
Promise1 -》 setTimeout2 -》 setTimeout1 -》 Promise2
我做错的原因也就是因为我对考点的不理解和混乱。
考点:
- 异步任务有哪些及其与同步任务的优先顺序、
- 任务队列里宏任务和微任务(优先顺序)
解析:
其实正确的答案顺序是
Promise1 -》 setTimeout1 -》 Promise2 -》 setTimeout2
1、异步任务
首先要知道.then()也是异步任务。且then属于微任务。
不信你运行这段代码看看:
1 Promise.resolve().then(()=>{
2 console.log('Promise1')
3 setTimeout(()=>{
4 console.log('setTimeout2')
5 },0)
6 });
7 console.log(1);
是不是先输出1,再输入的Promise1。
说明在铁定的同步任务“console.log(1)”之前,Promise.resolve()尽管是先运行,但是遇到then就被挂起到异步线程了。
所以,在上边面试题中,执行完第一行后,现在异步线程里边就是这个情况了:then被扔到了异步线程
》挂起异步任务后,代码接着往下走,到了第8行遇到setTimeout,这铁定的异步,继续挂到异步线程。
》再往下走,没有代码了,主线程空闲。
》于是开始处理任务队列的事件,此时任务队列里还有以下两个待执行任务:分别是then和setTimeout的回调函数。
》优先执行then()回调(第一个异步事件),执行回调函数里边的同步代码,console输出Promise1。
》then回调里边继续向下遇到第3行的setTimeout,挂起
》继续往下,then里边执行完毕,出栈。
》检查没有微任务了(下边没有then了),就处理第二个异步事件,就是第8行的那个定时器,然后执行。console出setTimeout1
》再往下立即执行同步函数Promise.resolve()
》遇到第10行的then,是异步,挂起。
》setTimeout1的回调执行完毕,出栈。
》事件循环前往事件队列找任务。此时事件队列长这样:
》检查有没有微任务,发现第10行的then2的回调,优先执行微任务then2 - 第三个异步事件。输出Promise2。(遇到微任务,即使setTimeout2排在前边也没用)
》继续检查没有微任务了(下边没有then了),没有出栈
》执行第四个异步事件,就是第3行的那个定时器,然后执行。console出setTimeout2。
2018.11.24 周六面试题:一道加大力度的宏任务、微任务题
console.log('begin') setTimeout(()=>{ console.log('a'); Promise.resolve().then(()=>{ console.log('b'); setTimeout(()=>{ console.log('c'); var arr1 = []; console.log(arr1 instanceof Array) }); }).then(()=>{ console.log('d'); }); },0); console.log('e')
2018.11.25 周日面试题:
console.log('========请按顺序写出以下程序执行结果=======') console.log('begin') setTimeout(()=>{ console.log('a'); Promise.resolve().then(()=>{ console.log('b1'); setTimeout(()=>{ console.log('c2'); }); }).then(()=>{ console.log('d2'); }); },0); console.log('e') var lis = document.getElementsByTagName('li);//假设页面有六个li元素 setTimeout(()=>{ console.log('=================3==============') console.log('类数组',lis,'转化的第一种方法:'); var arr1 = [],len1 = lis.length; for(var i = 0; i < len1; i++){ arr1.push(lis[i]); } console.log(arr1) console.log(arr1 instanceof Array) console.log('类数组',lis,'转化的第二种方法:'); console.log(Array.prototype.slice.call(lis)) console.log(Array.prototype.slice.call(lis) instanceof Array) });