文章首发于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

    1. 首先考到了破坏闭包结构,破坏闭包的方法很多,最简单的是将跨域变量转换成范围内的变量
    2. 其次考到了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

    关于这个输出,有如下几个逻辑:

    1. 4是setTimeOut.callback的输出,加入MacroTask末端,
    2. 输出1
    3. 执行Promise.resolve()将输出5的callback放到MicroTask中(注意这里不是MacroTask)
    4. 输出2
    5. 输出3
    6. MacroTask首个任务执行完毕
    7. 查找MicroTask里面有没有任务,发现有,执行,输出5
    8. 查找MacroTask里面有没有任务,发现有,执行,输出4
    9. 查找MicroTask里面有没有任务,发现没有,可以休息了
    10. 查找MacroTask里面有没有任务,发现没有,可以睡觉了
    11. 执行完毕

关于事件循环/关于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)为空的时候,开始依次执行:

  1. 把最早的任务(task A)放入任务队列
  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步
  3. 将 currently running task 设置为 task A
  4. 执行 task A (也就是执行回调函数)
  5. 将 currently running task 设置为 null 并移出 task A
  6. 执行 microtask 队列
    1. 在 microtask 中选出最早的任务 task X
    2. 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
    3. 将 currently running task 设置为 task X
    4. 执行 task X
    5. 将 currently running task 设置为 null 并移出 task X
    6. 在 microtask 中选出最早的任务 , 跳到 b
    7. 结束 microtask 队列
  7. 跳到第一步

上面就算是一个简单的 event-loop 执行模型

再简单点可以总结为:

  1. 在 macrotask 队列中执行最早的那个 task ,然后移出
  2. 执行 microtask 队列中所有可用的任务,然后移出
  3. 下一个循环,执行下一个 macrotask 中的任务 (再跳到第2步)

其他

  1. 当一个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 的情况
  2. microtask queue 中的 task 会在事件循环的当前回合中执行,因此 macrotask queue 中的 task 就只能等到事件循环的下一个回合中执行了
  3. 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

一道Javascript面试题引发的血案的更多相关文章

  1. 一道python面试题引发的血案

    这里说的是一道阿里校招的面试题:一行代码实现对列表a中的偶数位置的元素进行加3后求和? 今天去面试同样遇到了这个题目,这道题考察的是对python高阶函数map/filter的灵活运用(具体的使用方法 ...

  2. 一道JS面试题引发的血案

    刚入职新公司,属于公司萌新一枚,一天下午对着屏幕看代码架构时. BI项目组长给我看了一道面试别人的JS面试题. 虽然答对了,但把理由说错了,照样不及格. 话不多说,直接上题: var a = 1; s ...

  3. 一道 JavaScript 面试题

    有一道 JavaScript 面试题. f = function () { return true; }; g = function () { return false; }; (function() ...

  4. 一道试题引发的血案 int *ptr2=(int *)((int)a+1);

    某日,看到一道比较恶心的C语言的试题,考了很多比较绕的知识点,嘴脸如下: int main(void) { int a[4] = {1, 2, 3, 4}; int *ptr1=(int *)(&am ...

  5. 学生问的一道javascript面试题[来自腾讯]

    function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { demo: 5 }; this.show = function ...

  6. 一道Integer面试题引发的对Integer的探究

    面试题: //在jdk1.5的环境下,有如下4条语句: Integer i01 = 59; int i02 = 59; Integer i03 =Integer.valueOf(59); Intege ...

  7. 一道javascript面试题

    下面表达式比较的结果分别是什么? 1. []=="0" 2. []==0 3. "0"==0 4. []==false 5. []==[] 大家可以试试写下自己 ...

  8. 腾讯的一道JavaScript面试题

    //题目:分别弹出什么内容? <!-- function test(){ this.a = 1; alert(this); //[object Window] } test(); var t = ...

  9. 一道javascript面试题(闭包与函数柯里化)

    要求写一个函数add(),分别实现能如下效果: (1)console.log(add(1)(2)(3)(4)()); (2)console.log(add(1,2)(3,4)()); (3)conso ...

随机推荐

  1. IIS、Asp.net 编译时的临时文件路径

    IIS上部署的ASP.NET站点都会在一个.Net Framework的特定目录下生成临时编译文件增加ASP.NET站点的访问性能,有时候需要手动去删除这些临时编译文件,特别是发布新版本代码到IIS后 ...

  2. Noip2005谁拿了最多的奖学金题解

    题解 题目本身没什么好说的. 只是一道普及组的题让我领悟到scanf()读字符的真谛.scanf()函数最奇异的功能就是控制串里除格式化字符串之外的字符.若匹配成功则舍去. 所以我们能够"精 ...

  3. 加快Qemu Aarch32虚拟开发板的启动速度

    软件版本 Qemu: 2.8.0 虚拟开发板: vexpress-ca9 概述 之前的博文介绍了将Python移植到开发板上, 根文件系统采用的是ramdisk, 这个文件系统的缺点是修改的内容重启会 ...

  4. GPG入门教程

    原文地址:http://www.ruanyifeng.com/blog/2013/07/gpg.html 作者: 阮一峰 日期: 2013年7月12日 前两篇文章,我介绍了RSA算法. 今天,就接着来 ...

  5. Unity动画知识之二:Animator动画状态机

    上次我们讲过Unity游戏动画从入门到住院,今天我们来讲一下动画状态机. 好了,现在我们已经成功的导入了动画.接下来要玩的东西就很装13啦.因为大部分动画师是用不到这家伙的,需要掌握这个技能的,至少也 ...

  6. OPTAUTH 两步验证详解

    先贴图: 在对外网开放的后台管理系统中,使用静态口令进行身份验证可能会存在如下问题: (1) 为了便于记忆,用户多选择有特征作为密码,所有静态口令相比动态口令而言,容易被猜测和破解: (2) 黑客可以 ...

  7. VS Code搭建.NetCore开发环境(一)

    一.使用命令创建并运行.Net Core程序 1.dotnet new  xxx:创建指定类型的项目console,mvc,webapi 等 2.dotnet restore :加载依赖项 dotne ...

  8. .Net Standard 类库的创建和使用

    一..Net Standard类库的创建 1.在当前Vs 2017中创建.Net Standard 类库项目,目前版本默认值 .Net Standard v1.4 二..Net Standard类库的 ...

  9. C#和java之间的一些差异与共性

    C#与java之间的一些共性和差异整理 隐藏:与java中的重写几乎一致,但是需要添加new关键字让编译器知道,否则会有警告 虚方法:1.声明为virtual的方法就是虚方法,在子类中使用overri ...

  10. 硬盘杀手!Windows版Redis疯狂占用C盘空间!

    关键词:Redis占用C盘,Windows Redis,64位Windows版Redis疯狂占用C盘空间,redis启动后创建RedisQFolk_****.dat文件.redis-server.ex ...