Promise 高频面试题 + 满分答案

一、Promise 面试知识总图

text 复制代码
Promise
│
├── 1. 基础概念
│   ├── Promise 是什么
│   ├── 三种状态
│   ├── resolve / reject
│   ├── executor 是否异步
│   └── Promise 状态是否可逆
│
├── 2. then 链
│   ├── then 返回什么
│   ├── 为什么可以链式调用
│   ├── return 普通值
│   ├── return Promise
│   ├── throw Error
│   └── then / catch 错误传播
│
├── 3. Promise Resolution Procedure
│   ├── resolve(Promise)
│   ├── resolve(thenable)
│   ├── Promise.resolve()
│   └── Promise 的"状态跟随"
│
├── 4. 异步调度
│   ├── 微任务
│   ├── then 为什么异步
│   ├── Promise + setTimeout
│   └── async / await 与 Promise
│
├── 5. 静态方法
│   ├── Promise.all
│   ├── Promise.race
│   ├── Promise.allSettled
│   └── Promise.any
│
├── 6. 错误处理
│   ├── catch
│   ├── finally
│   ├── 未处理拒绝
│   └── 错误吞掉问题
│
├── 7. 手写 Promise
│   ├── Promise 构造器
│   ├── then
│   ├── 链式调用
│   ├── 异步回调
│   ├── thenable
│   └── 循环引用
│
└── 8. 工程问题
    ├── 并发控制
    ├── 请求超时
    ├── 请求取消
    ├── 重试
    ├── 请求竞态
    └── Promise 是否等于并行

二、第一层:Promise 基础高频题

1. Promise 是什么?

核心思路(一句话)

Promise 本质上就是一个表示异步操作最终结果的对象。这个结果要么成功,要么失败;我们可以通过 then、catch 等方式处理这个结果。

满分答案

它解决的核心问题不是"让 JavaScript 变成异步",而是:

把异步操作的成功结果、失败原因以及后续处理统一抽象成一个对象。

Promise 有三种状态:

text 复制代码
pending
   │
   ├── resolve(value) ──→ fulfilled
   │
   └── reject(reason) ──→ rejected

状态一旦从 pending 变成 fulfilled 或 rejected,就不能再次改变。

Promise 本身并不会让任务自动变成异步;例如:

javascript 复制代码
new Promise(() => {
  console.log('hello');
});

这里的 console.log 会同步执行。

真正异步的是:

javascript 复制代码
Promise.resolve().then(() => {
  console.log('hello');
});

因为 then 回调会进入微任务队列。(MDN Web Docs)

需要补一句

Promise 的核心价值是"异步结果的统一表示和组合",而不是单纯解决回调地狱。

这是比"Promise 是异步编程解决方案"更准确的回答。


三、Promise 有哪些状态?状态为什么不可逆?

2. Promise 状态有哪些?

满分答案

三个:

text 复制代码
pending    等待中
fulfilled  已成功
rejected  已失败

状态转换只有:

text 复制代码
             resolve
pending ─────────────→ fulfilled
   │
   │ reject
   ↓
rejected

不能:

text 复制代码
fulfilled → rejected
rejected  → fulfilled

例如:

javascript 复制代码
const p = new Promise((resolve, reject) => {
  resolve(1);
  reject(2);
});

p.then(console.log);

结果:

text 复制代码
1

因为 Promise 一旦完成,后续的 resolve/reject 不再改变它的最终状态。

关键追问

"那 resolve() 为什么还可以传 Promise?"

这就进入 Promise 最重要的高级知识:

resolve 不一定意味着"立即 fulfilled",它可能意味着"采用另一个 Promise 的最终状态"。

这是很多普通答案没有讲清楚的地方。


四、Promise 中最容易被问的:resolve 到底做了什么?

3. resolve(promise) 是立即成功吗?

核心思路

不一定。resolve(x) 的真正含义不是"把 Promise 成功值设置成 x",而是让当前 Promise 的状态跟随 x。

例如:

javascript 复制代码
const p1 = new Promise(resolve => {
  resolve(
    new Promise(resolve2 => {
      setTimeout(() => resolve2(1000), 1000);
    })
  );
});

p1.then(console.log);

最终:

text 复制代码
1秒后
1000

不是:

text 复制代码
Promise { ... }

原理

可以理解成:

text 复制代码
resolve(x)
   │
   ├── x 是普通值
   │      ↓
   │   fulfilled(x)
   │
   ├── x 是 Promise
   │      ↓
   │   跟随它的状态
   │
   └── x 是 thenable
          ↓
      调用它的 then
          ↓
      跟随它最终状态

这就是 Promise 的 Promise Resolution Procedure(承诺解析过程)。

Promise.resolve() 同样会处理 thenable,并跟随其最终状态。(MDN Web Docs)


五、Promise.resolve() 为什么是高频追问?

4. Promise.resolve() 和 new Promise() 有什么区别?

满分答案

最核心区别:

javascript 复制代码
Promise.resolve(value)

主要用于:

把一个值统一转换成 Promise。

例如:

javascript 复制代码
Promise.resolve(100)

相当于得到:

text 复制代码
fulfilled(100)

而:

javascript 复制代码
new Promise((resolve, reject) => {
  // 自己控制什么时候成功或失败
});

主要用于:

创建一个由自己控制完成时机的 Promise。

例如:

javascript 复制代码
const p = new Promise(resolve => {
  setTimeout(() => resolve(100), 1000);
});

一个非常重要的细节

javascript 复制代码
const p = Promise.resolve(1);

console.log(p);

这里得到的是一个 Promise。而:

javascript 复制代码
Promise.resolve(p) === p

在原生 Promise 场景下可以为 true。但如果传入的是其他 Promise 实现或者 thenable,就需要经过解析过程。


六、then 是 Promise 面试的核心

5. then() 返回什么?

核心思路

then() 一定返回一个新的 Promise,这个新 Promise 的状态和值由回调函数的执行结果决定。

例如:

javascript 复制代码
const p1 = Promise.resolve(1);

const p2 = p1.then(value => {
  return value + 1;
});

console.log(p2);

结果:

text 复制代码
p2 → fulfilled(2)

所以:

text 复制代码
p1
 │
 │ then
 ↓
p2
 │
 │ then
 ↓
p3

这就是 Promise 链。


七、then 返回普通值、Promise、抛异常分别是什么结果?

6. then() 回调返回不同东西会怎么样?

这是必须掌握的核心题。

情况一:返回普通值

javascript 复制代码
Promise.resolve(1)
  .then(value => {
    return value + 1;
  })
  .then(console.log);

结果:

text 复制代码
2

本质:

text 复制代码
return 2
   ↓
新 Promise fulfilled(2)

情况二:返回 Promise

javascript 复制代码
Promise.resolve(1)
  .then(() => {
    return Promise.resolve(2);
  })
  .then(console.log);

结果:

text 复制代码
2

不是:

text 复制代码
Promise { 2 }

因为新 Promise 会采用返回 Promise 的最终状态。


情况三:抛异常

javascript 复制代码
Promise.resolve(1)
  .then(() => {
    throw new Error('error');
  })
  .catch(console.error);

结果:

text 复制代码
rejected

可以把它记成:

text 复制代码
then 回调:

return 普通值
      ↓
新 Promise fulfilled

return Promise
      ↓
新 Promise 跟随它

throw Error
      ↓
新 Promise rejected

这张图是 Promise 链最重要的知识。


八、为什么 Promise 可以链式调用?

7. Promise 链式调用的底层原因是什么?

满分答案

不是因为 Promise 有什么特殊语法,而是因为:

then() 每次都会返回一个新的 Promise。

所以:

javascript 复制代码
p
  .then(fn1)
  .then(fn2)
  .then(fn3);

实际上是:

text 复制代码
p
 ↓
then(fn1)
 ↓
p2
 ↓
then(fn2)
 ↓
p3
 ↓
then(fn3)
 ↓
p4

每个 then 都在处理上一个 Promise 的结果。所以 Promise 链实际上是一条:

"异步结果传递链"。


九、then(success, error) 和 catch() 有什么区别?

8. then(null, error) 和 catch(error) 一样吗?

满分答案

对于错误处理来说:

javascript 复制代码
promise.catch(errorHandler)

本质上等价于:

javascript 复制代码
promise.then(undefined, errorHandler)

但面试时必须注意一个非常重要的区别:

javascript 复制代码
promise
  .then(
    value => {
      throw new Error('error');
    },
    error => {
      // 这里捕获不到上面 success 回调抛出的错误
    }
  );

因为第二个参数处理的是:

当前 Promise 的 rejection

而不是:

第一个 success 回调执行过程中产生的异常。

所以更常见的写法:

javascript 复制代码
promise
  .then(value => {
    throw new Error('error');
  })
  .catch(error => {
    console.log(error);
  });

一句话记忆

text 复制代码
then(success, error)
             ↑
       只能处理前一个 Promise

then(success)
      ↓
如果 success 自己出错
      ↓
返回的新 Promise rejected
      ↓
后面的 catch 才能捕获

十、finally() 到底怎么工作?

9. finally() 有什么特点?

核心思路

finally() 不关心成功还是失败,它主要用于执行无论如何都要执行的清理逻辑。

例如:

javascript 复制代码
fetch('/api')
  .then(...)
  .catch(...)
  .finally(() => {
    loading = false;
  });

最大的面试点

javascript 复制代码
Promise.resolve(1)
  .finally(() => {
    return 2;
  })
  .then(console.log);

输出:

text 复制代码
1

因为 finally 正常返回一个普通值时,不会改变原来的结果。

但:

javascript 复制代码
Promise.resolve(1)
  .finally(() => {
    throw new Error('finally error');
  })
  .catch(console.log);

会变成 rejected。

所以:

text 复制代码
finally 正常结束
      ↓
保持原来的成功/失败结果

finally 抛异常
      ↓
覆盖原结果 → rejected

finally 返回 rejected Promise
      ↓
覆盖原结果 → rejected

十一、Promise + Event Loop

10. 为什么 Promise.then() 是异步的?

javascript 复制代码
console.log(1);

Promise.resolve().then(() => {
  console.log(2);
});

console.log(3);

结果:

text 复制代码
1
3
2

原因

then 回调不会立即执行,而是进入微任务队列。

text 复制代码
同步代码
   ↓
调用 then
   ↓
注册回调
   ↓
当前同步代码执行完
   ↓
执行微任务
   ↓
then 回调执行

所以:

javascript 复制代码
Promise.resolve().then(fn);

即使 Promise 已经 fulfilled:

text 复制代码
fn 也不会同步执行

这是 Promise 保证异步回调一致性的关键行为。(MDN Web Docs)


十二、经典执行顺序题

11. 下面代码输出什么?

javascript 复制代码
console.log(1);

setTimeout(() => {
  console.log(2);
}, 0);

Promise.resolve().then(() => {
  console.log(3);
});

console.log(4);

答案:

text 复制代码
1
4
3
2

推导

text 复制代码
同步任务:
1
4

↓

微任务:
3

↓

下一轮任务:
2

注意点

不要简单回答:

"Promise 比 setTimeout 快。"

更准确的是:

Promise 的 then 回调属于微任务,而 setTimeout 回调属于任务队列中的定时器任务;当前同步代码执行完成后,会先处理微任务,再进入后续任务。


十三、async / await 本质是什么?

12. async/await 和 Promise 什么关系?

核心思路

async/await 不是另一套异步模型,它是建立在 Promise 之上的语法形式。

例如:

javascript 复制代码
async function test() {
  const result = await getData();
  return result;
}

本质上可以理解为:

text 复制代码
Promise
   ↓
await 暂停当前 async 函数后续执行
   ↓
Promise fulfilled
   ↓
恢复后续执行

并且:

javascript 复制代码
async function test() {
  return 100;
}

返回的仍然是 Promise:

javascript 复制代码
test().then(console.log);

输出:

text 复制代码
100

而:

javascript 复制代码
async function test() {
  throw new Error('error');
}

返回的是 rejected Promise。


十四、await 到底是不是阻塞?

13. await 会阻塞 JavaScript 主线程吗?

标准答案:不会。

await 只是暂停:

当前 async 函数后面的代码。

不会阻塞整个 JavaScript 主线程。例如:

javascript 复制代码
async function test() {
  console.log(1);

  await Promise.resolve();

  console.log(2);
}

test();

console.log(3);

输出:

text 复制代码
1
3
2

所以:

text 复制代码
await
 ↓
暂停当前 async 函数
 ↓
把后续逻辑安排到 Promise 后续调度中
 ↓
主线程继续执行其他同步代码

十五、Promise.all 是最高频的静态方法题

14. Promise.all() 有什么特点?

核心思路

所有任务都成功才成功,只要一个失败就失败。

javascript 复制代码
Promise.all([
  getUser(),
  getOrders(),
  getProducts()
]);

适合:

多个任务互不依赖,并且必须全部成功才能继续。

例如:

text 复制代码
页面初始化
├── 用户信息
├── 权限信息
└── 配置信息

全部成功
   ↓
页面渲染

最容易错的地方

javascript 复制代码
Promise.all([p1, p2, p3])

如果 p2 失败:

text 复制代码
Promise.all → rejected

但是:

p1、p3 并不会因此自动取消。

Promise.all 只是让聚合结果提前 rejected ,不是取消底层任务。(MDN Web Docs)


十六、Promise.all 的结果顺序是什么?

15. Promise.all 为什么结果顺序和完成顺序不一样?

javascript 复制代码
const p1 = new Promise(resolve => {
  setTimeout(() => resolve('A'), 3000);
});

const p2 = new Promise(resolve => {
  setTimeout(() => resolve('B'), 1000);
});

Promise.all([p1, p2]).then(console.log);

结果:

javascript 复制代码
['A', 'B']

不是:

javascript 复制代码
['B', 'A']

因为:

Promise.all 按输入顺序组织结果,而不是按完成顺序。

所以:

text 复制代码
p1 ─────── A
p2 ─ B

结果:
[A, B]

十七、Promise.all / race / allSettled / any 怎么区别?

16. 四个并发方法怎么选?

这是必须形成条件反射的一道题。

方法 成功条件 失败条件 典型场景
all 全部成功 任意一个失败 必须全部成功
race 第一个 settled 第一个 settled 是失败 谁先结束用谁
allSettled 全部结束 不会 rejected 需要知道所有结果
any 第一个成功 全部失败 谁先成功用谁

核心记忆:

text 复制代码
all
→ 全部成功

race
→ 第一个结束

allSettled
→ 全部结束

any
→ 第一个成功

MDN 对四者的定义也是这一核心差异。(MDN Web Docs)


十八、Promise.race 和 Promise.any 最容易混淆

17. race() 和 any() 有什么区别?

race 看:

谁最先结束。

无论成功还是失败。

text 复制代码
A reject ── 100ms
B fulfill ─ 200ms

race
 ↓
reject

any 看:

谁最先成功。

text 复制代码
A reject ── 100ms
B fulfill ─ 200ms

any
 ↓
B

只有:

text 复制代码
所有 Promise 都失败

时:

javascript 复制代码
Promise.any(...)

才 rejected,并且错误是 AggregateError。(MDN Web Docs)


十九、Promise.race 能实现请求超时吗?

18. Promise.race() 如何实现请求超时?

经典答案:

javascript 复制代码
function timeout(promise, ms) {
  const timeoutPromise = new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error('请求超时'));
    }, ms);
  });

  return Promise.race([
    promise,
    timeoutPromise
  ]);
}

使用:

javascript 复制代码
timeout(fetch('/api'), 5000);

但是高级开发必须指出一个问题

这段代码只能实现:

"5 秒后返回超时结果"

并不意味着:

"5 秒后真正取消网络请求"。

因为 Promise.race() 不会自动取消输掉竞争的 Promise。(MDN Web Docs)

生产环境更合理的是:

javascript 复制代码
const controller = new AbortController();

const timer = setTimeout(() => {
  controller.abort();
}, 5000);

fetch('/api', {
  signal: controller.signal
}).finally(() => {
  clearTimeout(timer);
});

这是高级面试非常喜欢追问的点:

Promise 的"结果竞争"和"底层任务取消"是两个不同问题。


二十、Promise.allSettled 解决什么问题?

19. 为什么需要 Promise.allSettled()?

假设:

javascript 复制代码
Promise.all([
  upload1(),
  upload2(),
  upload3()
]);

其中一个失败。Promise.all() 会直接 rejected。但如果业务需求是:

三个文件全部上传完以后,我要知道每一个到底成功还是失败。

就应该:

javascript 复制代码
const results = await Promise.allSettled([
  upload1(),
  upload2(),
  upload3()
]);

结果类似:

javascript 复制代码
[
  {
    status: 'fulfilled',
    value: 'success'
  },
  {
    status: 'rejected',
    reason: new Error('失败')
  },
  {
    status: 'fulfilled',
    value: 'success'
  }
]

所以:

text 复制代码
all
→ 要求整体成功

allSettled
→ 要完整知道整体结果

二十一、Promise.any 的真实业务场景

20. Promise.any() 有什么实际用途?

非常典型:

多个 CDN / 服务节点同时请求,哪个先成功就使用哪个。

javascript 复制代码
const result = await Promise.any([
  fetch('https://cdn-a.com/data'),
  fetch('https://cdn-b.com/data'),
  fetch('https://cdn-c.com/data')
]);

特点:

text 复制代码
A 失败
B 失败
C 成功
 ↓
使用 C

如果:

text 复制代码
A 失败
B 失败
C 失败

才整体失败。


二十二、Promise 是否等于并发?

21. Promise 是并发还是并行?

这是非常重要的一题。

Promise 本身不等于并行。

JavaScript 主线程在同一时刻仍然只能执行一段 JavaScript。Promise 主要解决的是:

异步任务的组织、组合和结果传递。

例如:

javascript 复制代码
const p1 = fetch('/a');
const p2 = fetch('/b');
const p3 = fetch('/c');

await Promise.all([p1, p2, p3]);

这里可以表现出:

text 复制代码
请求 A ────────────────┐
请求 B ───────────┐    │
请求 C ───────────────┐│
                     ↓↓↓
                  Promise.all

这属于异步并发。但不能因此说:

"三个 JavaScript 函数同时在 CPU 上执行。"

真正的 CPU 并行通常需要 Worker 等机制。(MDN Web Docs)


二十三、一个非常高频的坑:await 串行

22. 下面代码有什么性能问题?

javascript 复制代码
const user = await getUser();
const orders = await getOrders();
const products = await getProducts();

如果三个请求互不依赖:

text 复制代码
getUser
  ↓
完成
  ↓
getOrders
  ↓
完成
  ↓
getProducts

属于串行。

可以改成:

javascript 复制代码
const [user, orders, products] = await Promise.all([
  getUser(),
  getOrders(),
  getProducts()
]);

变成:

text 复制代码
getUser    ──────────┐
getOrders  ────────┐ │
getProducts ────────┐│
                    ↓↓↓
                 all

但不要机械使用 Promise.all

如果:

javascript 复制代码
const user = await getUser();
const orders = await getOrders(user.id);

存在依赖关系,就不能简单并发。所以真正的判断标准是:

有没有数据依赖,而不是"看到 await 就改 Promise.all"。


二十四、Promise 手写题

23. 手写一个 Promise,面试官到底在考什么?

真正考的不是:

"你能不能写一个 Promise。"

而是:

text 复制代码
状态管理
↓
异步回调保存
↓
then 链
↓
返回新 Promise
↓
结果传递
↓
异常传播
↓
thenable
↓
循环引用

所以面试如果让你手写 Promise,至少应该能逐层实现。


二十五、简化版 Promise

第一版:

javascript 复制代码
class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    const resolve = value => {
      if (this.state !== 'pending') return;

      this.state = 'fulfilled';
      this.value = value;
    };

    const reject = reason => {
      if (this.state !== 'pending') return;

      this.state = 'rejected';
      this.reason = reason;
    };

    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }
}

这只能说明:

text 复制代码
Promise 状态只能改变一次

但还不够。


二十六、真正的 Promise 手写核心:then

需要增加:

javascript 复制代码
then(onFulfilled, onRejected) {
  return new MyPromise((resolve, reject) => {
    // 根据当前 Promise 状态处理
  });
}

如果当前是:

text 复制代码
pending

就必须保存:

javascript 复制代码
onFulfilled
onRejected

等未来状态改变以后再执行。所以 Promise 内部最核心的数据结构可以理解为:

text 复制代码
Promise
│
├── state
├── value / reason
│
├── fulfilled callbacks
│
└── rejected callbacks

二十七、手写 Promise 时最关键的难点:then 返回新 Promise

例如:

javascript 复制代码
const p2 = p1.then(value => {
  return value + 1;
});

这里:

text 复制代码
p1
 ↓
then
 ↓
p2

所以:

不能让 then 返回当前 Promise。

必须:

javascript 复制代码
return new MyPromise(...)

然后根据:

javascript 复制代码
onFulfilled(value)

的结果决定 p2:

text 复制代码
普通值
   ↓
resolve(value)

Promise
   ↓
跟随 Promise

throw
   ↓
reject(error)

这就是 Promise 链的核心。


二十八、手写 Promise 最容易漏掉:thenable

例如:

javascript 复制代码
const thenable = {
  then(resolve) {
    setTimeout(() => {
      resolve(100);
    }, 1000);
  }
};

如果:

javascript 复制代码
return thenable;

Promise 链也应该最终得到:

text 复制代码
100

而不是把整个对象当成结果。因此 Promise 的解析逻辑实际上要判断:

text 复制代码
x
│
├── 普通值
│
├── Promise
│
└── thenable

这也是 Promise 标准里非常重要的一部分。(MDN Web Docs)


二十九、Promise 循环引用

24. 为什么下面代码有问题?

javascript 复制代码
let p2 = p1.then(() => {
  return p2;
});

这里形成:

text 复制代码
p2
 ↑
 │
then 返回 p2

也就是:

text 复制代码
Promise 自己等待自己

如果实现 Promise 时不检测这种情况,就可能永远 pending。所以一个完整 Promise 实现需要处理:

javascript 复制代码
if (promise2 === x) {
  return reject(
    new TypeError('Chaining cycle')
  );
}

这就是为什么真正的 Promise 手写题远比"写三个状态"复杂。


三十、Promise 错误传播

25. Promise 为什么可以"一直 catch 到最后"?

例如:

javascript 复制代码
Promise.resolve()
  .then(() => {
    throw new Error('A');
  })
  .then(() => {
    console.log('B');
  })
  .then(() => {
    console.log('C');
  })
  .catch(error => {
    console.log(error.message);
  });

输出:

text 复制代码
A

因为:

text 复制代码
throw
 ↓
当前 then 返回 Promise rejected
 ↓
下一个 then 没有 rejection handler
 ↓
继续向后传递
 ↓
catch 捕获

所以可以把 Promise 的错误传播理解成:

text 复制代码
rejected
   ↓
找下一个 rejection handler
   ↓
没有?
   ↓
继续向后传
   ↓
直到 catch

三十一、Promise 的错误为什么有时候"消失"?

26. 下面代码有什么问题?

javascript 复制代码
Promise.reject(new Error('error'))
  .catch(error => {
    console.log(error);
  });

这里错误实际上已经被处理了。

但如果:

javascript 复制代码
Promise.reject(new Error('error'))
  .catch(error => {
    console.log(error);
    return 'success';
  });

后面的 Promise 会:

text 复制代码
fulfilled('success')

也就是说:

catch 不只是"打印错误",它也是一个 Promise 链节点,可以把 rejected 转换成 fulfilled。

这是非常重要的。


三十二、Promise 的并发控制

27. 如果 1000 个请求只能同时运行 10 个,怎么实现?

应该设计:

text 复制代码
任务池
│
├── Worker 1
├── Worker 2
├── ...
└── Worker 10

队列:
1 2 3 4 5 ... 1000

核心思想:

text 复制代码
当前运行数 < 10
      ↓
取任务
      ↓
执行
      ↓
完成
      ↓
运行数 - 1
      ↓
继续取任务

一个可面试的实现

javascript 复制代码
async function mapLimit(list, limit, worker) {
  const results = new Array(list.length);
  let nextIndex = 0;

  async function run() {
    while (true) {
      const index = nextIndex++;

      if (index >= list.length) {
        return;
      }

      results[index] = await worker(
        list[index],
        index
      );
    }
  }

  const workers = Array.from(
    { length: Math.min(limit, list.length) },
    run
  );

  await Promise.all(workers);

  return results;
}

使用:

javascript 复制代码
const result = await mapLimit(
  urls,
  10,
  url => fetch(url)
);

这就体现了:

Promise 负责异步结果组织,业务代码负责控制并发度。


三十三、Promise 重试

28. 如何实现请求失败自动重试?

javascript 复制代码
async function retry(fn, times) {
  let lastError;

  for (let i = 0; i < times; i++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
    }
  }

  throw lastError;
}

使用:

javascript 复制代码
const data = await retry(
  () => fetch('/api'),
  3
);

继续追问:

是不是所有请求都适合重试?

答案:

不是。

例如:

text 复制代码
GET
查询
幂等操作

通常更适合重试。

而:

text 复制代码
POST 创建订单
支付
扣库存

需要考虑:

text 复制代码
幂等性
请求是否已经在服务端执行
重复提交风险

所以:

重试不是 Promise API 问题,而是分布式系统里的幂等性问题。

这个回答层次会明显高于单纯写代码。


三十四、Promise 请求竞态

29. 前端搜索框为什么会出现 Promise 竞态?

例如用户快速输入:

text 复制代码
a
ab
abc

请求:

text 复制代码
a    ───────────────→ 结果 A
ab   ───────→ 结果 B
abc  ──→ 结果 C

如果:

text 复制代码
C 先回来
A 最后回来

最终页面可能被:

text 复制代码
A

覆盖。

这就是:

请求竞态。

解决方案包括:

text 复制代码
防抖
取消旧请求
请求序列号
只接受最后一次请求结果

例如:

javascript 复制代码
let requestId = 0;

async function search(keyword) {
  const id = ++requestId;

  const result = await fetch(`/search?q=${keyword}`);

  if (id !== requestId) {
    return;
  }

  render(result);
}

更彻底的方式可以结合:

text 复制代码
AbortController

取消旧请求。


三十五、Promise 取消问题

30. Promise 能取消吗?

Promise 本身没有通用的取消语义。

例如:

javascript 复制代码
const p = fetch('/api');

你不能简单:

javascript 复制代码
p.cancel();

因为 Promise 主要描述的是:

结果状态

而不是:

底层任务生命周期。

因此要取消真正的异步任务,需要任务本身支持取消。

浏览器里最典型的是:

javascript 复制代码
const controller = new AbortController();

fetch('/api', {
  signal: controller.signal
});

controller.abort();

所以必须区分:

text 复制代码
Promise
→ 结果抽象

AbortController
→ 取消信号

fetch
→ 真正执行任务

这是高级面试非常重要的抽象边界。


三十六、Promise 内存问题

31. Promise 会造成内存泄漏吗?

准确说:

Promise 本身不是天然的内存泄漏源,但长期 pending 的 Promise + 持续注册的回调可能导致对象长期无法释放。

例如:

javascript 复制代码
const pending = new Promise(() => {});

for (let i = 0; i < 10000; i++) {
  pending.then(() => {
    console.log(i);
  });
}

如果这个 Promise 永远 pending:

text 复制代码
pending Promise
   │
   ├── callback 1
   ├── callback 2
   ├── callback 3
   └── ...

这些回调会一直挂在 Promise 上。

MDN 也特别指出,某些组合方法在长期 pending Promise 上反复注册 handler,可能导致 handler 累积;Promise 本身没有提供通用的"取消这些 handler"机制,因此更应该取消底层操作。(MDN Web Docs)


三十七、Promise 最容易出现的几个"错误理解"

32. 高级面试一定要主动纠正这些说法

错误 1

Promise 就是异步。

不准确。

正确:

Promise 是异步结果的抽象;Promise executor 本身可以同步执行。


错误 2

Promise.all 会取消其他请求。

错误。

正确:

Promise.all 只会让聚合 Promise 提前 rejected,不会自动取消其他任务。

(MDN Web Docs)


错误 3

Promise.race 谁最快就返回谁。

不完整。正确:

谁最先 settled 谁赢,包括 fulfilled 和 rejected。


错误 4

Promise.any 谁最快就返回谁。

错误。正确:

谁最先 fulfilled 谁赢;只有全部 rejected 才失败。


错误 5

await 会阻塞线程。

错误。正确:

await 暂停当前 async 函数的后续执行,不会阻塞 JavaScript 主线程。


错误 6

Promise 就是并行。

错误。正确:

Promise 可以组织异步并发,但 Promise 本身不等于 CPU 并行。


三十八、Promise 高频题优先级

如果你准备的是高级前端岗位,我建议不要平均用力。

S级:必须达到"能原理 + 能手写 + 能追问"

text 复制代码
★★★★★
1. Promise 三种状态
2. then 为什么返回新 Promise
3. then 返回普通值 / Promise / throw
4. Promise Resolution Procedure
5. resolve(Promise) 为什么会跟随状态
6. Promise.resolve
7. Promise 链
8. Promise 错误传播
9. 微任务
10. async / await
11. Promise.all
12. all / race / allSettled / any
13. Promise.race + 超时
14. Promise 是否可以取消
15. 手写 Promise

A级:大厂非常容易继续追问

text 复制代码
★★★★☆
16. Promise.all 是否取消其他任务
17. Promise.all 结果为什么按输入顺序
18. await 串行与并发
19. Promise.any + AggregateError
20. thenable
21. finally
22. 未处理 Promise rejection
23. 请求竞态
24. Promise 并发控制
25. Promise 重试
26. AbortController

B级:高级岗位用于拉开差距

text 复制代码
★★★☆☆
27. Promise 长期 pending 的资源问题
28. Promise.race 的 handler 累积
29. 取消与 Promise 状态的边界
30. 幂等性与 Promise 重试
31. 请求超时与真正取消的区别
32. 微任务过多导致任务饥饿
33. async/await 错误边界
34. 自定义 Promise / thenable 兼容
35. Promise 链循环引用

三十九、最后给你一套真正适合 高级前端的"满分答案"

如果面试官问:

"你系统讲一下 Promise。"

不要从:

"Promise 有三种状态......"

机械开始。建议直接这样回答:

Promise 本质上是对异步操作最终结果的统一抽象,核心解决的是异步结果的传递、组合和错误处理,而不是简单地把代码变成异步。

我通常从四个层面理解 Promise:

第一,状态。

Promise 有 pending、fulfilled、rejected 三种状态,并且状态只能从 pending 转换到最终状态,不能逆转。

第二,链式传递。

then() 每次都会返回一个新的 Promise,所以可以形成 Promise 链。而 then 回调的结果会决定这个新 Promise:

  • 返回普通值 → 新 Promise fulfilled
  • 返回 Promise / thenable → 新 Promise 跟随它的最终状态
  • 抛出异常 → 新 Promise rejected

这其实就是 Promise 链能够不断传递结果和错误的核心。

第三,异步调度。

then 回调不会同步执行,而是进入微任务队列。因此 Promise 经常和 Event Loop 一起考。

async/await 也是建立在 Promise 之上的语法形式,await 只会暂停当前 async 函数,不会阻塞整个 JavaScript 主线程。

第四,Promise 的组合。

常见的四个静态方法分别解决不同问题:

text 复制代码
Promise.all
→ 全部成功才成功

Promise.race
→ 第一个 settled 的结果

Promise.allSettled
→ 等全部结束,并收集所有结果

Promise.any
→ 第一个成功的结果

但这里有一个容易被忽略的点:这些方法解决的是结果组合,不等于底层任务取消。

例如 Promise.all() 某个任务失败后,其他请求并不会自动取消;Promise.race() 超时后,输掉竞争的请求也不会自动停止。如果业务需要真正取消底层任务,需要使用任务本身提供的取消能力,例如 AbortController。

所以从工程角度看,我会把 Promise 理解成:

text 复制代码
异步任务
   ↓
Promise:统一表示最终结果
   ↓
then / catch / finally:处理结果
   ↓
all / race / allSettled / any:组合多个结果
   ↓
async / await:让 Promise 链更接近同步代码的写法
   ↓
AbortController / 并发控制 / 重试:
解决真正的工程问题

如果继续深入 Promise 的底层实现,核心就是三个东西:

text 复制代码
状态管理
+
回调队列
+
Promise Resolution Procedure

其中最关键的是 Promise Resolution Procedure,因为它决定了 resolve(普通值)、resolve(Promise)、resolve(thenable) 以及 then 返回不同结果时,新的 Promise 到底应该进入什么状态。

所以我认为 Promise 真正需要掌握的不是 API 背诵,而是:

"状态如何流转 → 结果如何传递 → 异常如何传播 → 多个异步任务如何组合 → 底层任务如何取消和控制。"

把这条链路理解清楚,Promise 绝大多数面试题其实都是它的不同应用。


最后压缩成一张"面试脑图"

text 复制代码
                    Promise
                       │
        ┌──────────────┼──────────────┐
        ↓              ↓              ↓
      状态            链式调用        并发组合
        │              │              │
 pending             then           all
   ↓                  ↓              race
fulfilled       普通值/Promise       allSettled
rejected            /throw          any
        │              │
        └──────┬───────┘
               ↓
        Promise Resolution
               │
      ┌────────┼────────┐
      ↓        ↓        ↓
    value    Promise   thenable
               │
               ↓
         跟随最终状态

               ↓
         Event Loop
               │
               ↓
          Microtask
               │
               ↓
         async / await

               ↓
        工程问题
   ┌───────────┼───────────┐
   ↓           ↓           ↓
并发控制      超时        取消
   ↓           ↓           ↓
任务池      race       AbortController
   │
   └────→ 重试 / 竞态 / 幂等性

如果按高级前端的要求准备,Promise 最值得深挖的其实不是再背几十道 API 题,而是下面这条追问链:

Promise 是什么 → 为什么需要状态 → then 为什么返回新 Promise → return Promise 为什么会跟随状态 → thenable 是什么 → Promise Resolution Procedure 怎么处理 → 为什么 then 是微任务 → async/await 怎么建立在 Promise 上 → all/race/any/allSettled 如何组合 → 为什么组合不等于取消 → 如何做超时/取消/并发控制/重试/竞态处理 → 手写 Promise。

这条链打通,才真正接近高级前端 的 Promise 水平。 (MDN Web Docs)

相关推荐
liangshanbo121525 天前
面试题终极整理:手写 Promise.all / Promise.allSettled
前端·javascript·promise
ynchyong1 个月前
JavaScript Promise 实战:.then() 与 .catch() 的区别及回调函数封装指南
开发语言·javascript·ecmascript·promise·then
今日无bug1 个月前
从同步阻塞到 async/await:一文梳理 JS 异步流程控制的进化史
javascript·promise
今日无bug2 个月前
JS 同步与异步:从单线程到 Promise
javascript·promise
wordbaby2 个月前
前端请求缓存写了,同一个接口为什么还是打了十几次?
前端·promise
Thanks_ks3 个月前
从零手写一个 Promise:深入理解 JavaScript 异步编程的本质
javascript·promise·事件循环·异步编程·手写源码·前端进阶·promises/a+
To_OC3 个月前
别再串行写 await 了,Promise.all 才是并行请求的正确打开方式
前端·javascript·promise
HjhIron3 个月前
Node.js 内置模块之 path 与 fs:从路径拼接到异步演进
node.js·promise
想要成为糕糕手3 个月前
前端异步编程的终极指南:从 Promise 到 async/await,这一次彻底搞懂!
javascript·promise