一道 Javascript 面试题引发的血案
为了面试新人我们公司准备了一些面试题, 几个同事都尝试做了一下, 结果大片玩家 HP-100000
覆盖的考点不多, 题目挺有意思, 建议手动执行一边玩玩.
for (var i = 0; i < 5; i++) { setTimeout(function () { console.log(i); }, 1000); } console.log(i);
- Q: 这道题目会输出什么?
- A: 这道题目还比较简单, 如果对 Javascript 稍微有一点深入的同学都会发现这道题目循环里面出现了闭包, 因此输出的数字是完全相同的, 最后的输出也是完全相同的.
- 考点: 闭包, (伪) 异步
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 关键字 1
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 usinglet
cannot be used before its declaration or an error will result.. -
考点: 闭包, (伪) 异步, 作用域
同样是 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 后输出
-
考点: 闭包, (伪) 异步, 作用域, 事件队列
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 是一个回调函数, 因此无论延时多少结果都是最后输出.结果是 1, 2, 3
- 考点: (伪) 异步, 事件队列
这道题目其实是其他地方抄袭来的 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: 关于这个输出, 有如下几个逻辑:
2. 4 是
setTimeOut.callback
的输出, 加入MacroTask
末端, 2. 输出 1 3. 执行Promise.resolve()
将输出 5 的 callback 放到MicroTask
中 (注意这里不是MacroTask
)- 输出 2
- 输出 3
MacroTask
首个任务执行完毕- 查找
MicroTask
里面有没有任务, 发现有, 执行, 输出 5 - 查找
MacroTask
里面有没有任务, 发现有, 执行, 输出 4 - 查找
MicroTask
里面有没有任务, 发现没有, 可以休息了 - 查找
MacroTask
里面有没有任务, 发现没有, 可以睡觉了 - 执行完毕
闭包什么的不想写了
一个事件循环 (EventLoop) 中会有一个正在执行的任务 (Task), 而这个任务就是从 macrotask 队列中来的. 在 whatwg 规范中有 queue 就是任务队列. 当这个 macrotask 执行结束后所有可用的 microtask 将会在同一个事件循环中执行, 当这些 microtask 执行结束后还能继续添加 microtask 一直到真个 microtask 队列执行结束. 1
基本来说, 当我们想以同步的方式来处理异步任务时候就用 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 被创建. 比如下面两个
promiseA.then()
的回调就是一个 taskpromiseA
是resolved
或rejected
: 那这个 task 就会放入当前事件循环回合的microtask queue
promiseA
是pending
: 这个 task 就会放入 事件循环的未来的某个 (可能下一个) 回合的microtask queue
中setTimeout
的回调也是个 task , 它会被放入macrotask queue
即使是 0ms 的情况
microtask queue
中的 task 会在事件循环的当前回合中执行, 因此macrotask queue
中的 task 就只能等到事件循环的下一个回合中执行了- click ajax setTimeout 的回调是都是 task, 同时, 包裹在一个 script 标签中的 js 代码也是一个 task 确切说是 macrotask.
关于本文
文章标题 | 一道 Javascript 面试题引发的血案 |
发布日期 | 2017-04-03 |
文章分类 | Tech |
相关标签 | #Javascript |
留言板
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER
PLACE_HOLDER