JavaScript 一面:Promise、async/await 与常见手写题

事件循环那一篇更多讲“宏任务/微任务”的整体模型,这一篇更聚焦在 Promise/async/await 本身的考点,以及一面里常见的几道手写题。

一面里关于 Promise 的基础问题,通常围绕这几点:

  • 三种状态:pending / fulfilled / rejected;
  • 状态一旦从 pending 变成 fulfilled 或 rejected,就不可再变;
  • then 可以返回新的 Promise,从而形成链式调用;
  • 错误可以通过 throw 或返回 rejected Promise 传递给后续的 catch

简单例子:

 1Promise.resolve(1)
 2  .then((v) => {
 3    console.log(v); // 1
 4    return v + 1;
 5  })
 6  .then((v) => {
 7    console.log(v); // 2
 8    throw new Error("boom");
 9  })
10  .catch((err) => {
11    console.log(err.message); // "boom"
12  });

答题时,能清晰描述“链式调用 + 错误如何在链上传播”,基本就过了基础线。

async/await 可以看作是 Promise 的语法糖:

  • async 函数总是返回一个 Promise;
  • await 等价于“在 then 回调里拿到结果”,只是写法更接近同步;
  • try/catch 包裹可以捕获 await 链路上的错误。

简单示意:

 1async function fetchData() {
 2  try {
 3    const res = await fetch("/api/data");
 4    const json = await res.json();
 5    return json;
 6  } catch (e) {
 7    console.error("请求失败", e);
 8    throw e; // 继续向外传播
 9  }
10}

一面里常见的追问是:
forEach + async 有什么坑?”、“async 函数返回的是什么?”等。

题目:手写一个 myAll(promises),接收一个 Promise 数组,全部成功时按顺序返回结果,有任何一个失败就直接 reject。

参考实现(简化版):

 1function myAll(promises) {
 2  return new Promise((resolve, reject) => {
 3    if (!Array.isArray(promises)) {
 4      return reject(new TypeError("Argument must be an array"));
 5    }
 6
 7    const results = [];
 8    let count = 0;
 9
10    if (promises.length === 0) {
11      return resolve(results);
12    }
13
14    promises.forEach((p, index) => {
15      Promise.resolve(p)
16        .then((value) => {
17          results[index] = value;
18          count++;
19          if (count === promises.length) {
20            resolve(results);
21          }
22        })
23        .catch(reject);
24    });
25  });
26}

答题时重点解释两个点:

  • 需要按照原顺序输出结果,因此用 index 存放;
  • 需要计数,所有 Promise 完成后再 resolve,任意一个失败则立刻 reject

题目:给定一组返回 Promise 的函数,按顺序串行执行它们,把每一步的结果放在数组里返回。

参考实现:

 1function runInSequence(tasks) {
 2  const results = [];
 3
 4  return tasks
 5    .reduce((prev, task) => {
 6      return prev.then(() => {
 7        return task().then((res) => {
 8          results.push(res);
 9        });
10      });
11    }, Promise.resolve())
12    .then(() => results);
13}

或者用 async/await 版本:

1async function runInSequence(tasks) {
2  const results = [];
3  for (const task of tasks) {
4    const res = await task();
5    results.push(res);
6  }
7  return results;
8}

关键在于说明:

  • 不要用 forEach + async,因为它不会等待;
  • 要么用 reduce 链 Promise,要么用 for...of + await

题目:实现一个简化版的调度器 Scheduler,同一时间最多并发执行 limit 个任务。

参考实现思路(简化):

 1class Scheduler {
 2  constructor(limit) {
 3    this.limit = limit;
 4    this.running = 0;
 5    this.queue = [];
 6  }
 7
 8  add(task) {
 9    return new Promise((resolve, reject) => {
10      const run = () => {
11        this.running++;
12        task()
13          .then(resolve, reject)
14          .finally(() => {
15            this.running--;
16            if (this.queue.length > 0) {
17              const next = this.queue.shift();
18              next();
19            }
20          });
21      };
22
23      if (this.running < this.limit) {
24        run();
25      } else {
26        this.queue.push(run);
27      }
28    });
29  }
30}

解释要点:

  • 用一个队列存待执行任务;
  • running 计数当前运行中的任务数,当小于 limit 时可以立刻执行,否则入队;
  • 每个任务结束后,从队列中取出下一个任务执行。

示例题:

1const tasks = [1, 2, 3];
2
3async function run() {
4  tasks.forEach(async (n) => {
5    await new Promise((r) => setTimeout(r, n * 1000));
6    console.log(n);
7  });
8  console.log("done");
9}

参考答案:

  • 这段代码会先输出 "done",然后陆续输出 1, 2, 3,而不是等三个异步完成之后再输出 "done"
  • 原因是 forEach 不感知回调中的 Promise,run 函数里的 console.log("done") 不会等待回调执行;
  • 如果要按顺序等待,可以改成:
1async function run() {
2  for (const n of tasks) {
3    await new Promise((r) => setTimeout(r, n * 1000));
4    console.log(n);
5  }
6  console.log("done");
7}

或使用前面的串行执行工具函数。

参考答案要点:

  • Promise 可以通过 .catch 捕获错误,错误会沿着 then 链向后传播;
  • async/await 可以把异步写成同步风格,用 try/catch 包裹 await 来捕获错误,更直观:
1async function main() {
2  try {
3    const data = await fetchData();
4  } catch (e) {
5    // 统一处理
6  }
7}
  • 底层上没有本质差别,async 函数依然是返回一个 Promise,只是写法更易读。

可以直接给出前面那段 myAll 的核心代码,并强调两个点:

  • Promise.resolve(p) 兼容非 Promise 值;
  • 用数组按索引存结果,配合计数判断“都完成了”再 resolve。

面试官更看重你能不能把“并发执行 + 收集结果”的思路讲清楚,而不是一定要求完整实现 Promise/A+ 规范。