AI 大模型也会答错的一段 JS 代码片段

182 阅读3分钟

分享 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)、setTimeoutPromiseasync/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
*/