今晚在掘金大佬的文章里看到一道题,稍加改造,并有点自己的浅见可以记录一下。
大佬的文章 https://juejin.im/post/5eb55ceb6fb9a0436748297d ,题就是那照片中的第3题。
我稍加改造,加入了 Promise ,以体现 微任务和宏任务 的知识点。
function Counter() {
var start = Date.now();
this.num = 0;
this.timer1 = setInterval(function() {
this.num++;
var gap = Date.now() - start;
Promise.resolve(2020).then(res => {
this.num++;
gap -= res;
})
console.log('timer1', this.num, gap);
}, 996);
JSON.parse('{"desc":"...."}'); // 假设解析耗时 1024 毫秒
this.timer2 = setTimeout(() => {
this.num++;
var gap = Date.now() - start;
Promise.resolve(2020).then(function (res) {
this.num++;
gap -= res;
})
console.log('timer2', this.num, gap);
}, 0);
}
new Counter(); // 写出前 10 条输出
// Counter(); // 如果直接调用呢? 也写出前 10 条输出
了解JS执行机制的话,这题不难吧。大概的有哪些坑,我列一下:
- this指向,这个真是 “逢面必考” 哇
- 自增自减运算符,其实相当于
a = a + 1
,本质还是赋值,所以可以执行,同时涉及到数据类型的隐式转换。 - 定时器函数是同步执行的,但是其回调函数是推入 Event Table ,等到时间到了,就推入 Event Queue ,再要等到主线程空闲,事件轮询机制才把它拿到主线程去执行。
setInterval()
不是重复执行,而是每隔所设定的时间,就生成一个回调,推入事件队列,每次回调里的变量都是新的。同时,第一次回调的执行也是在等待设定的时间之后的,不要搞混。哦,还有,如果主线程一直没空,那么setInterval()
可不管,默默地一直往事件队列里推入回调,所以很可能,等主线程有空了,Event Queue 里已经堆满了它的回调,哈哈哈哈- 然后是箭头函数,它内部不绑定
this
,而是使用 它定义时的 上下文this,所以不管你之后是在哪调用,它内部的this
还是要往它定义时的作用域里找,其实就是说创建这个箭头函数的地方,我们创建函数一般是不会写new Function()
的,所以要理解写字面量的() => {}
这就是创建了一个函数实例对象。题中setTimeout()
里的箭头函数回调,是在什么时候创建的呢?当然是Counter()
被调用被执行的时候咯,所以this
指向就不用多说了吧。 - 再然后就是我自己加了个
Promise
,主要是需要注意.then()
里的回调也是异步的哦,而且是微任务。微任务就是宏任务拿到主线程执行完了,顺便也把同一个作用域内的微任务执行掉,就像吃大餐吃饱了,其实还可以再扒几口饭后甜点的,哈哈。。。对了,还要看到里面箭头函数跟普通匿名函数的区别,注意this
指向哦。 - 最后,还有
996 + 1024 = 2020
,不能错过哦,哈哈
最后再记录一下个人的一点浅见。如果我是面试官,可能会觉得这题能分三层:
- 搞不清楚
this
指向,或者认为this.num++
或gap -= res
会报错的,在第一层,直接pass - 完整写出 10 条输出的,在第二层
- 写出 3~5 条输出就不写了的,更有意思,哈哈
为什么呢?因为 写出 3~5 条输出后,后面就都是按规律重复而已。这时候应该 向面试官解释为什么不写完10条:因为 定时器是不准的,gap
的值 会有 几个毫秒 的误差,每次执行都不一定保证结果一致。
然后就可以跟面试官扯一扯为什么定时器不准呢?因为 JS 是脚本语言,边解释边执行,而且是单线程的,异步任务会被推入异步队列,然后定时器的回调是异步任务里的宏任务,每次都要等到下一个 Event Loop 里,再被拿出来执行,而每个 Event Loop 消耗的时间可能是零点几毫秒,也可能是一两个毫秒,要看执行环境,所以,gap
的值每次都会有一点点误差。
最后再深入聊一聊 单线程、微任务和宏任务、事件队列、事件轮询机制、Event Loop ,等等,那跟面试官的PY不就好啦