JavaScript 一面:Promise、async/await 与常见手写题
事件循环那一篇更多讲“宏任务/微任务”的整体模型,这一篇更聚焦在 Promise/async/await 本身的考点,以及一面里常见的几道手写题。
Promise 基础:状态、then 链与错误传递
一面里关于 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:语法糖 + try/catch 错误处理
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 函数返回的是什么?”等。
常见手写题之一:实现一个简化版 Promise.all
题目:手写一个
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(限制并发为 1)
题目:给定一组返回 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时可以立刻执行,否则入队; - 每个任务结束后,从队列中取出下一个任务执行。
常见面试题与参考答案
题 1:forEach + async 有什么坑?应该怎么写?
示例题:
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}
或使用前面的串行执行工具函数。
题 2:Promise 和 async/await 在错误处理上有什么不同?
参考答案要点:
- 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,只是写法更易读。
题 3:如何实现一个简化版 Promise.all?(高频手写)
可以直接给出前面那段 myAll 的核心代码,并强调两个点:
- 用
Promise.resolve(p)兼容非 Promise 值; - 用数组按索引存结果,配合计数判断“都完成了”再 resolve。
面试官更看重你能不能把“并发执行 + 收集结果”的思路讲清楚,而不是一定要求完整实现 Promise/A+ 规范。