一道Javascript面试题引发的血案
文章首发于 szhshp的第三边境研究所 ,转载请注明
先来看几道面试题,公司的开发们都尝试做了一下,然而基本没有人能够全部答对。
覆盖的考点很多,也有一些难度,题目挺有意思建议手动执行一边玩玩。
Question 1
for (var i = 0; i <5 ; i++) { setTimeout(function(){ console.log(i) ),1000} } console.log(i)
- Q:这道题目会输出什么?
- A:这道题目还比较简单,如果对Javascript稍微有一点深入的同学都会发现这道题目循环里面出现了闭包,因此输出的数字是完全相同的,最后的输出也是完全相同的。
- 考点:闭包,(伪)异步
Question 2
for (let i = 0; i <5 ; i++) { //注意var变成了let setTimeout(function(){ console.log(i) },1000) } console.log(i)
- Q:这道题目会输出什么?
-
A:这道题目其实是个坑。首先题目与Q1的区别就是变量i的定义改为了关键字let,使用let的时候会将变量限制在循环之中,因此第二个输出其实会报错。另外setTimeout实现了(伪)异步,同时因为let将变量作用域进行了控制,破坏了闭包结构,因此会按照正常顺序输出。
关于let关键字[^3]
Use the let statement to declare a variable, the scope of which is restricted to the block in which it is declared. You can assign values to the variables when you declare them or later in your script.
A variable declared using let cannot be used before its declaration or an error will result.. -
考点:闭包,(伪)异步,作用域
Question 3
同样是Q1的代码
for (var i = 0; i <5 ; i++) { //DO NOT MODIFY setTimeout(function(){ //DO NOT MODIFY console.log(i) },1000) } console.log(i) //DO NOT MODIFY
- Q:修改上述代码(部分行不允许修改,可以在代码间插入),以实现“每隔一秒输出一个数字并且顺序为0-5”
-
A
- 首先考到了破坏闭包结构,破坏闭包的方法很多,最简单的是将跨域变量转换成范围内的变量
-
其次考到了setTimeout事件队列的处理
for (var i = 0; i <5 ; i++) { (function(i){ setTimeout(function(){ console.log(i) },1000*i) })(i) //将i作为参数传入匿名函数,如此破坏了闭包内跨域访问 } setTimeout(function (){ console.log(i); }, 5000); //强行将5放到5sec后输出
-
考点:闭包,(伪)异步,作用域,事件队列
Question 4
window.setTimeout(function (){ console.log(2) },1); //Ouput for a long time for (var i = 0; i < 1000; i++) { console.log(''); }; console.log(1) window.setTimeout(function (){ console.log(3) },0);
- Q:这道题目会输出什么?
- A:可能有些同学会记得,setTimeout是一个回调函数,因此无论延时多少结果都是最后输出。
- 考点:(伪)异步,事件队列
Question 5
这道题目其实是其他地方抄袭来的[^2],正好和之前考点有一定重叠因此一起放了过来:
setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) //time consuming ops for( var i=0 ; i<10000 ; i++ ){ i==9999 && resolve(); } console.log(2) }).then(function(){ console.log(5) }); console.log(3);
- Q:这道题目会输出什么?
-
A:输出是12354
关于这个输出,有如下几个逻辑:
- 4是setTimeOut.callback的输出,加入MacroTask末端,
- 输出1
- 执行Promise.resolve()将输出5的callback放到MicroTask中(注意这里不是MacroTask)
- 输出2
- 输出3
- MacroTask首个任务执行完毕
- 查找MicroTask里面有没有任务,发现有,执行,输出5
- 查找MacroTask里面有没有任务,发现有,执行,输出4
- 查找MicroTask里面有没有任务,发现没有,可以休息了
- 查找MacroTask里面有没有任务,发现没有,可以睡觉了
- 执行完毕
关于事件循环/关于macrotask和microtask[^1]
简介
一个事件循环(EventLoop)中会有一个正在执行的任务(Task),而这个任务就是从 macrotask 队列中来的。在whatwg规范中有 queue 就是任务队列。当这个 macrotask 执行结束后所有可用的 microtask 将会在同一个事件循环中执行,当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束。
怎么用
基本来说,当我们想以同步的方式来处理异步任务时候就用 microtask(比如我们需要直接在某段代码后就去执行某个任务,就像Promise一样)。
其他情况就直接用 macrotask。
两者的具体实现
- macrotasks: setTimeout setInterval setImmediate I/O UI渲染
- microtasks: Promise process.nextTick Object.observe MutationObserver
从规范中理解
规范:https://html.spec.whatwg.org/multipage/webappapis.html#task-queue
- 一个事件循环(event loop)会有一个或多个任务队列(task queue) task queue 就是 macrotask queue
- 每一个 event loop 都有一个 microtask queue
- task queue == macrotask queue != microtask queue
- 一个任务 task 可以放入 macrotask queue 也可以放入 microtask queue 中
- 当一个 task 被放入队列 queue(macro或micro) 那这个 task 就可以被立即执行了
再来回顾下事件循环如何执行一个任务的流程
当执行栈(call stack)为空的时候,开始依次执行:
- 把最早的任务(task A)放入任务队列
- 如果 task A 为null (那任务队列就是空),直接跳到第6步
- 将 currently running task 设置为 task A
- 执行 task A (也就是执行回调函数)
- 将 currently running task 设置为 null 并移出 task A
- 执行 microtask 队列
- 在 microtask 中选出最早的任务 task X
- 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
- 将 currently running task 设置为 task X
- 执行 task X
- 将 currently running task 设置为 null 并移出 task X
- 在 microtask 中选出最早的任务 , 跳到 b
- 结束 microtask 队列
- 跳到第一步
上面就算是一个简单的 event-loop 执行模型
再简单点可以总结为:
- 在 macrotask 队列中执行最早的那个 task ,然后移出
- 执行 microtask 队列中所有可用的任务,然后移出
- 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)
其他
- 当一个task(在 macrotask 队列中)正处于执行状态,也可能会有新的事件被注册,那就会有新的 task 被创建。比如下面两个
1. promiseA.then() 的回调就是一个 task 1. promiseA 是 resolved或rejected: 那这个 task 就会放入当前事件循环回合的 microtask queue 1. promiseA 是 pending: 这个 task 就会放入 事件循环的未来的某个(可能下一个)回合的 microtask queue 中 1. setTimeout 的回调也是个 task ,它会被放入 macrotask queue 即使是 0ms 的情况
- microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了
- click ajax setTimeout 的回调是都是 task, 同时,包裹在一个 script 标签中的js代码也是一个 task 确切说是 macrotask。
参考文献
[^3]: let 语句 (JavaScript) .aspx)
[^2]: https://www.zhihu.com/question/36972010
[^1]: https://github.com/ccforward/cc/issues/48