分享 4 年前面试字节的一道真题,当时搞半天也没答对哈哈,一方面是浏览器事件循环(Event Loop)机制掌握不够,另一方面题目确实有点绕即使掌握相关知识也容易出错。
1、题目:下面代码的输出结果是什么?
let p = [];
(function () {
setTimeout(() => {
console.log('timeout 0');
}, 0);
let i = 0;
for (; i < 3; i++) {
p[i] = function () {
return new Promise(function (resolve) {
console.log(`promise ${i}`);
resolve(`promise ${i * i}`);
})
}
}
})();
async function b() {
console.log('async -1');
}
function a() {
console.log(`async ${p.length}`);
return async function () {
console.log(`async ${p.length}`);
await b();
console.log('async -2')
};
}
p.push(a());
p[1]().then(console.log);
p[3]();
2、让 AI 大模型做一做
读者可复制代码到 AI 大模型上跑一跑,结果有点意思
3、分析过程
这段代码包含多个部分,涉及立即执行函数表达式(IIFE)、setTimeout、Promise、async/await 和函数数组。让我们一步步分析代码的执行流程和输出结果。
(1) 初始化空数组 p:
let p = [];
(2) 立即执行函数表达式(IIFE):
(function () {
setTimeout(() => {
console.log('timeout 0');
}, 0);
let i = 0;
for (; i < 3; i++) {
p[i] = function () {
return new Promise(function (resolve) {
console.log(`promise ${i}`);
resolve(`promise ${i * i}`);
});
}
}
})();
上面立即执行函数会立刻执行,先是将设置的时间为 0 的 setTimeout 的回调添加到宏任务队列里面,在这里我们模拟记录一下当前的宏任务队列:
const macrotask = [
() => console.log('timeout 0')
]
接着 for 循环将三个函数(每个函数返回一个 Promise)添加到数组 p 的前三个位置(p[0], p[1], p[2])。for 循环里面的函数是个闭包,闭包获取到上层作用域的值是最后的值,因此,p 中每个函数里面的 i 的值均为 3。
(3) 定义 async 函数 b
async function b() {
console.log('async -1');
}
(4) 定义函数 a 并返回一个 async 函数
function a() {
console.log(`async ${p.length}`);
return async function () {
console.log(`async ${p.length}`);
await b();
console.log('async -2');
};
}
(5) 执行 p.push(a())
这段代码先执行 a(),在函数 a 中,因为此时的 p 是上面上面 for 循环生成的长度为 3 的数组,所以,全篇代码的第一输出就在这里,并且是 'async 3'。接着函数 a 返回一个 async 函数并 push 到数组 p 中,此时,p 的长度为 4。
(6) 执行 p[1]().then(console.log)
这段代码先是执行 p[1](),代码如下:
function () {
return new Promise(function (resolve) {
console.log(`promise ${i}`);
resolve(`promise ${i * i}`);
});
}
上面也提到了 i 的值是 3,因此这里输出 promise 3,并且 resolve 返回的值 'promise 9' 会在后面的 then 中取到,而 then 中的回调则会增加到微任务队列里面。在这里我们模拟记录一下当前的微任务队列:
const microtask = [
console.log('promise 9')
]
(7) 执行 p[3]()
p[3]() 是函数 a 中返回的 async 函数
async function () {
console.log(`async ${p.length}`);
await b();
console.log('async -2');
}
p 长度是 4,此时先是输出 async 4,接着执行 await b(),则是输出 async -1。在 await b() 后面的代码,其实相当于 b().then(console.log('async -2')),因此,此时的微任务队列(数组第一个元素表示队列尾)为:
const microtask = [
console.log('async -2'),
console.log('promise 9'),
]
(8) 同步代码执行完后,会先将微任务队列里面的回调全部执行
因此,接下来是 microtask 队列里面的回调出队执行,结果是 'promise 9'、'async -2'。
(9) 在清空微任务队列后,会去看宏任务队列是否有回调,如果有,则取一个出来执行
这里的宏任务队列是:
const macrotask = [
() => console.log('timeout 0')
]
因此,输出 'timeout 0'。此时,再也没有别的微任务或者宏任务加进来,因此后面就结束了时间循环。
4、正确答案
因此,chrome 浏览器输出的答案:
/* 输出
async 3
promise 3
async 4
async - 1
promise 9
async - 2
timeout 0
*/