Promise

23 阅读21分钟

JS Promise 复习笔记

JS Promise 复习笔记 & 题库

用于 JS 面试/复习的一份 Promise 速查手册 + 题库(可直接放到任意笔记软件里)


目录

  1. 一、Promise 基础知识
  2. 二、Promise 进阶知识
  3. 三、面试高频考点 Checklist
  4. 四、Promise 题库(含参考答案)
  5. 五、附录:典型代码片段

一、Promise 基础知识

1.1 Promise 是什么

  • Promise 是一个表示异步操作最终结果的对象
  • 用来解决:
    • 回调地狱(callback hell)
    • 回调难以组合、错误难以统一处理等问题。

关键特点:

  • 有三种状态:pending / fulfilled / rejected
  • 状态一旦从 pending 变为 fulfilledrejected不可逆
  • 通过 .then() / .catch() / .finally() 注册回调。

最常见的创建方式:

```js const p = new Promise((resolve, reject) => { // executor 执行器:同步执行 if (/* 成功 */) { resolve(value); // 让 p 变为 fulfilled } else { reject(reason); // 让 p 变为 rejected } }); ```


1.2 三种状态与状态流转

  • pending:进行中
  • fulfilled:已成功
  • rejected:已失败

状态流转:

  • pending -> fulfilled(调用 resolve
  • pending -> rejected(调用 reject 或抛出异常)
  • 一旦变为 fulfilledrejected,就锁死,不会再变(不可逆)。

1.3 then / catch / finally

1.3.1 then
p.then(onFulfilled, onRejected);
  • then 返回一个新的 Promise(注意不是原来的那个)。
  • 返回值处理规则:
    • onFulfilled/onRejected 返回 普通值 → 新 Promise 变为 fulfilled,值就是返回值。
    • 返回一个 Promise → 新 Promise 状态跟随该 Promise。
    • 回调中 抛出异常 → 新 Promise 变为 rejected。
  • 任意一个参数如果不是函数,则会被忽略(值穿透)。
1.3.2 catch
p.catch(onRejected);
  • 等价于 .then(null, onRejected)
  • 通常用于链尾统一错误处理。
1.3.3 finally
p.finally(() => {
  // 不接收上一个结果
});
  • 无论成功还是失败都会执行。
  • 不会改变链上传递的值/错误(除非 finally 里抛错或返回一个拒绝的 Promise)。

1.4 执行顺序与微任务

关键点:

  • new Promise(executor) 里的 executor 立即同步执行
  • then/catch/finally 注册的回调会放入 微任务队列(microtask),在当前宏任务结束后、下一个宏任务开始前执行。
  • setTimeout 回调是 宏任务

示例:

console.log(1);
const p = new Promise((resolve) => {
  console.log(2);
  resolve();
  console.log(3);
});
p.then(() => console.log(4));
console.log(5);

执行顺序:

  1. 同步:1 2 3 5
  2. 微任务:4

最终输出:1 2 3 5 4


二、Promise 进阶知识

2.1 Promise 链与值传递

Promise.resolve(1)
  .then(x => {
    console.log(x);   // 1
    return x + 1;     // 返回 2
  })
  .then(x => {
    console.log(x);   // 2
    return Promise.resolve(x + 1); // 返回一个 Promise
  })
  .then(x => {
    console.log(x);   // 3
  });

特点:

  • 每个 .then 都会返回一个 新的 Promise
  • 上一个回调的返回值会传入下一个 .then
  • “值穿透”:如果 then 没有传对应的回调(比如 .then(null, onRejected)),或者参数不是函数,状态和值会原样传下去

2.2 错误处理与错误冒泡

Promise.resolve()
  .then(() => {
    throw new Error('err in then');
  })
  .then(() => {
    console.log('不会执行');
  })
  .catch(err => {
    console.log('catch:', err.message); // err in then
  })
  .then(() => {
    console.log('错误处理之后继续执行');
  });

要点:

  • 任意一个 then 中抛出的异常,会被最近的 catch 捕获。
  • catch 本身也返回 Promise,可以继续链式调用。
  • catch 后如果不再抛错,链会恢复为 fulfilled 状态。

2.3 Promise 静态方法

2.3.1 Promise.resolve / Promise.reject
Promise.resolve(42); // 立即得到一个 fulfilled 的 Promise
Promise.reject('error'); // 立即得到一个 rejected 的 Promise
  • Promise.resolve(promiseLike)
    • 如果是 Promise,直接返回;
    • 如果是 thenable,会“吸收”其结果;
    • 普通值则包装成 fulfilled Promise。
2.3.2 Promise.all
Promise.all([p1, p2, p3])
  .then(values => {
    // 所有成功,values 是结果数组
  })
  .catch(err => {
    // 任意一个失败,立即 rejected,err 是第一个失败的原因
  });
  • 并发执行,等全部成功;只要有一个失败就整体失败。
  • 结果顺序与数组顺序一致,与完成先后无关。
  • 有任一 Promise 一直 pending,则整体一直 pending。
2.3.3 Promise.race
Promise.race([p1, p2, p3])
  .then(value => {
    // 第一个 settled 的结果,无论是成功还是失败
  })
  .catch(err => {
    // 若第一个是失败
  });

适合做超时控制等场景。

2.3.4 Promise.allSettled
Promise.allSettled([p1, p2, p3])
  .then(results => {
    // 每一项都有 status: 'fulfilled' | 'rejected'
    // 以及 value / reason
  });
  • 等所有 Promise 都 settled(成功或失败)
  • 永远返回 fulfilled,结果里包含每项的 statusvalue/reason
2.3.5 Promise.any
Promise.any([p1, p2, p3])
  .then(value => {
    // 第一个 fulfilled 的结果
  })
  .catch(aggregateError => {
    // 所有都 rejected,才会进入这里
  });
  • 只要有一个 Promise fulfilled,就立即 fulfilled。
  • 如果所有都 rejected,就返回一个 AggregateError

2.4 事件循环与微任务顺序

典型面试题代码:

console.log('start');

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

Promise.resolve()
  .then(() => {
    console.log('promise1');
  })
  .then(() => {
    console.log('promise2');
  });

console.log('end');

执行顺序:

  1. 同步:startend
  2. 微任务:promise1promise2
  3. 宏任务:timeout

最终:start, end, promise1, promise2, timeout


2.5 async / await 与 Promise 的关系

  • async 函数 一定返回一个 Promise
  • await 后面可以跟 Promise 或普通值:
    • 如果是 Promise,会等待其 settled,然后得到结果或抛出错误;
    • 如果是普通值,相当于 Promise.resolve(这个值)
async function foo() {
  try {
    const res = await fetchData();
    return res + 1;
  } catch (e) {
    console.error(e);
    throw e; // 重新抛出,让调用者处理
  }
}
  • await 只是在语法上“看起来同步”,本质上还是基于 Promise + 微任务。

2.6 常见实战模式(简单版)

2.6.1 串行执行多个异步任务
const tasks = [api1, api2, api3];

tasks.reduce(
  (prev, cur) => prev.then(cur),
  Promise.resolve()
);
2.6.2 简易并发限制(思路)
  • 核心:维护一个“正在执行中的 Promise 数量”,超出就排队。
  • 面试时可以说清楚思路,或简单写出控制队列的版本(不必非常完善)。
2.6.3 Promise 实现超时
function withTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
  return Promise.race([promise, timeout]);
}

三、面试高频考点 Checklist

自测一下下面这些点是不是都能顺畅讲出来

  1. Promise 三种状态 & 状态不可逆。
  2. new Promise 中 executor 是同步执行的。
  3. then/catch 返回新 Promise,返回值如何影响后续状态。
  4. 值穿透 & 错误冒泡。
  5. 微任务 vs 宏任务,几段常见代码的执行顺序。
  6. Promise.all / race / allSettled / any 的差异和使用场景。
  7. async/await 的本质,错误如何捕获,如何并发多个请求。
  8. 如何手写简易 Promise 核心(状态机、回调队列、异步执行)。
  9. 实战题:实现超时、重试、并发限制等。

四、Promise 题库(含参考答案)

建议先自己做,再对答案。

4.1 基础理解题


Q1:Promise 有哪几种状态?状态之间如何变化?

参考答案:

  • 状态:pendingfulfilledrejected
  • 流转:pending -> fulfilled / pending -> rejected,且只会发生一次;状态一旦变为 fulfilled 或 rejected 就不可逆,不会回到 pending,也不会在 fulfilled / rejected 之间互相切换。

Q2:new Promise(fn) 中传入的函数 fn 是同步还是异步执行?

参考答案:

  • fn(executor)在构造 Promise 时立即同步执行
  • then/catch 中的回调才是异步的(放入微任务队列)。

Q3:then 的两个参数分别是干什么的?如果只传一个呢?

参考答案:

  • 第一个参数:成功回调 onFulfilled
  • 第二个参数:失败回调 onRejected
  • 如果只传一个回调(通常是成功回调),失败会“冒泡”到后面的 catch,相当于该 then 没有处理错误。

Q4:catch.then(null, onRejected) 有什么区别?

参考答案:

  • 行为基本等价:都是在链上处理 rejected 状态。
  • catch 可读性更好,也更符合统一错误处理习惯,链式调用时更清晰。

Q5:finally 会改变 Promise 的结果吗?

参考答案:

  • 一般不会。finally 不接收上一个结果,执行完之后仍然会把之前的值/错误继续向下传。
  • 只有在 finally 内部抛出错误,或者返回一个 rejected Promise 时,才会改变后续链的状态。

Q6:Promise 回调是微任务还是宏任务?

参考答案:

  • then / catch / finally 注册的回调是 微任务(microtask)

Q7:Promise.resolve(promiseLike) 会做什么?

参考答案:

  • 如果参数是 Promise:直接返回这个 Promise;
  • 如果是 thenable 对象:会按照其 then 方法“吸收”其状态和结果;
  • 如果是普通值:返回一个 fulfilled Promise,value 为该值。

Q8:Promise.all([]) 传空数组会怎样?

参考答案:

  • 立即返回一个 fulfilled Promise,结果为 []

Q9:Promise.allPromise.race 的关键区别?

参考答案:

  • Promise.all
    • 等待列表中所有 Promise 完成;
    • 只要有一个 rejected,整体就 rejected;
    • 结果数组顺序与传入顺序一致。
  • Promise.race
    • 谁先 settled(不管成功或失败),就采用谁的结果/错误;
    • 常用于超时控制等场景。

Q10:async 函数默认返回什么?函数内部的返回值会怎样?

参考答案:

  • async 函数返回一个 Promise。
  • 函数内部 return x 等价于 return Promise.resolve(x)

4.2 代码执行顺序 / 输出题


Q11:下面代码输出什么?

console.log(1);

const p = new Promise((resolve) => {
  console.log(2);
  resolve();
  console.log(3);
});

p.then(() => console.log(4));

console.log(5);

参考答案:

  • 输出顺序:1 2 3 5 4
  • 解析:
    • 同步部分:console.log(1)235
    • resolve() 只是将 then 回调放入微任务队列,不会立刻执行;
    • 同步任务执行完后,执行微任务 → 打印 4

Q12:下面代码输出什么?

Promise.resolve()
  .then(() => {
    console.log('A');
    throw new Error('err');
  })
  .then(() => {
    console.log('B');
  })
  .catch(() => {
    console.log('C');
  })
  .then(() => {
    console.log('D');
  });

参考答案:

  • 输出:A C D
  • 解析:
    • 第一层 then:打印 A,然后抛出错误 → 后面的 then 被跳过;
    • 错误被 catch 捕获 → 打印 C
    • catch 处理完后返回的是 fulfilled 状态(除非再次抛错),所以最后一个 then 执行 → 打印 D

Q13:下面代码输出什么?

console.log('start');

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

Promise.resolve()
  .then(() => {
    console.log('promise1');
  })
  .then(() => {
    console.log('promise2');
  });

console.log('end');

参考答案:

  • 输出:start, end, promise1, promise2, timeout
  • 解析:
    • 同步:startend
    • 微任务:promise1promise2
    • 宏任务(下一个事件循环):timeout

Q14:下面代码输出什么?

const p1 = Promise.resolve().then(() => {
  console.log('p1 then1');
  return 'p1';
});

p1.then(() => {
  console.log('p1 then2');
});

const p2 = Promise.resolve().then(() => {
  console.log('p2 then1');
});

console.log('sync');

参考答案:

  • 输出顺序:sync, p1 then1, p2 then1, p1 then2
  • 解析:
    • 同步阶段:只输出 sync
    • 微任务队列入队顺序:
      • 第一次 Promise.resolve().then(p1 then1);
      • 第二次 Promise.resolve().then(p2 then1);
    • 依次执行微任务:
      • 执行 p1 then1(打印 p1 then1),返回值 'p1'p1.then 的回调(p1 then2)入队;
      • 执行 p2 then1(打印 p2 then1);
      • 执行新入队的 p1 then2(打印 p1 then2)。

Q15:下面的日志顺序?

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

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

async1();

new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

参考答案:

输出顺序:

  1. 同步:
    • script start
    • async1 start
    • async2
    • promise1
    • script end
  2. 微任务:
    • async1 end(await 之后的部分)
    • promise2
  3. 宏任务:
    • setTimeout

最终:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

Q16:下面会打印几次 “ok”?

const p = new Promise((resolve) => {
  resolve('ok');
  resolve('again'); // 无效
});

p.then(console.log);
p.then(console.log);

参考答案:

  • 打印两次 "ok"
  • 解析:
    • 状态只以第一次 resolve 为准;
    • 同一个 Promise 可以被多次 then,每个 then 都会执行,对应相同的结果。

Q17:输出什么?

Promise.reject('error')
  .then(() => {
    console.log('then');
  })
  .catch(err => {
    console.log('catch', err);
  })
  .then(() => {
    console.log('after catch');
  });

参考答案:

  • 输出:
catch error
after catch
  • 解析:
    • 初始 rejected → 直接跳过第一个 then
    • catch 捕获错误并打印;
    • catch 之后如果没有抛错,则链恢复为 fulfilled → 后面的 then 继续执行。

Q18:输出什么?

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log);

参考答案:

  • 输出:1
  • 解析:
    • then 的参数如果不是函数(例如数字、Promise 实例),会被忽略;
    • 等价于:
Promise.resolve(1)
  .then(x => x)
  .then(x => x)
  .then(console.log);
  • 整条链都在传递 1

Q19:Promise.all 中如果有一个 Promise 一直 pending,会发生什么?

参考答案:

  • Promise.all 返回的 Promise 会一直保持 pending,既不会 fulfilled 也不会 rejected。

Q20:下面代码的输出?

let count = 0;

function test() {
  return new Promise(resolve => {
    count++;
    if (count === 1) {
      resolve(test());
    } else {
      resolve('done');
    }
  });
}

test().then(console.log);

参考答案:

  • 输出:done
  • 解析:
    • 第一次调用 test()count = 1resolve(test()),参数是一个 Promise;
    • 内部再次调用 test()count = 2resolve('done')
    • 外层 Promise 状态跟随内层 Promise,最终结果为 'done'

4.3 手写 & 开放题(面试高频)

下面这些题可以自己写一写,再对照思路/答案。


Q21:实现一个 sleep(ms) 函数,用 Promise 形式使用:

sleep(1000).then(() => console.log('wake up'));

参考实现:

function sleep(ms) {
  return new Promise(resolve => {
    setTimeout(resolve, ms);
  });
}

Q22:实现一个带超时的请求封装 requestWithTimeout(promise, ms)

思路:

  • 使用 Promise.race,将真实请求与一个“超时 Promise”一起竞争。

参考实现:

function requestWithTimeout(promise, ms) {
  const timeout = new Promise((_, reject) => {
    setTimeout(() => reject(new Error('Timeout')), ms);
  });
  return Promise.race([promise, timeout]);
}

Q23:用 Promise 封装一个 Node-style 回调函数(如 fs.readFile)成 readFilePromise

通用 promisify 思路:

function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn(...args, (err, data) => {
        if (err) reject(err);
        else resolve(data);
      });
    });
  };
}

Q24:手写一个简易版 Promise(说出关键点即可)

面试时可从结构上描述:

  1. 内部维护三个状态:pendingfulfilledrejected,初始为 pending
  2. 维护两个回调队列:onFulfilledCallbacks / onRejectedCallbacks
  3. resolve / reject
    • 如果状态仍是 pending,则切换状态;
    • 按顺序执行对应队列中的回调;
  4. then(onFulfilled, onRejected)
    • 返回一个新的 Promise;
    • 根据当前状态:
      • 如果已 fulfilled / rejected,异步执行对应回调;
      • 如果 pending,把回调推入队列;
    • 处理回调返回值:
      • 普通值 → resolve 新 Promise;
      • Promise → 采用其结果(thenable 处理);
      • 抛错 → reject 新 Promise。

class SimplePromise {
  constructor(executor) {
    // 1. 初始化状态
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;

    // 2. 存放回调的数组 (发布订阅)
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];

    // 3. 定义 resolve
    const resolve = (value) => {
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 还没执行的回调,统统执行
        this.onResolvedCallbacks.forEach(fn => fn());
      }
    };

    // 4. 定义 reject
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    // 5. 立即执行 executor
    try {
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }

  // ------------------------------------------
  // 简化版 then:只管传值,不管递归
  // ------------------------------------------
  then(onFulfilled, onRejected) {
    // 参数兜底 (值穿透)
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };

    // 为了链式调用,必须返回新 Promise
    return new SimplePromise((resolve, reject) => {
      
      // 封装一个通用处理函数
      const handle = (callback, data) => {
        // 使用 setTimeout 模拟异步 (微任务太长,用宏任务模拟即可)
        setTimeout(() => {
          try {
            // 1. 执行用户的回调,拿到返回值 x
            const x = callback(data);
            // 2. 【核心简化】直接把 x 传给下一个 resolve
            // (标准版这里需要递归解析 x,这里省略了,直接当做普通值传递)
            resolve(x); 
          } catch (err) {
            reject(err); // 报错就走 reject
          }
        });
      };

      // 状态判断
      if (this.status === 'fulfilled') {
        handle(onFulfilled, this.value);
      } else if (this.status === 'rejected') {
        handle(onRejected, this.reason);
      } else {
        // Pending 状态:订阅!存入队列
        this.onResolvedCallbacks.push(() => handle(onFulfilled, this.value));
        this.onRejectedCallbacks.push(() => handle(onRejected, this.reason));
      }
    });
  }
}

Q25:使用 async/await 并发请求两个接口 api1api2,并在两个都完成后再处理结果。

参考实现:

async function getData() {
  const [res1, res2] = await Promise.all([api1(), api2()]);
  // do something with res1, res2
  return { res1, res2 };
}
  • 对比错误写法(串行):
const a = await api1();
const b = await api2(); // 这里 api2 一定在 api1 完成后才开始

Q26:怎么用 Promise 实现“重试机制”,最多重试 3 次?

参考实现:

function retry(fn, times) {
  return new Promise((resolve, reject) => {
    function attempt(remaining) {
      fn()
        .then(resolve)
        .catch(err => {
          if (remaining <= 0) {
            reject(err);
          } else {
            attempt(remaining - 1);
          }
        });
    }
    attempt(times);
  });
}
  • 思路:
    • 包一层外部 Promise;
    • 内部使用递归 attempt,失败且还有次数就继续尝试。

Q27: 实现红绿灯

const light = (duration, color) => {
  return new Promise((resolve) => {
    console.log(`${color} start`);
    setTimeout(() => {
      console.log(`${color} end`);
      resolve();
    }, duration);
  });
};

const run = () => {
  light(2000, 'red')
    .then(() => light(2000, 'green'))
    .then(() => light(1000, 'yellow'))
    .then(run); // 或者 .then(() => run())
};

run();

进阶版本

const light = (duration, color) => {
  return new Promise((resolve) => {
    console.log(`${color} start`);
    setTimeout(() => {
      console.log(`${color} end`);
      resolve();
    }, duration);
  });
};

async function run() {
  while (true) {
    await light(2000, 'red');
    await light(2000, 'green');
    await light(1000, 'yellow');
  }
}

run();

Q28: 实现Fetch的超时控制

Q29: 实现并发控制

要求实现一个 asyncPool(limit, tasks) 函数

一、 核心解题思路:自助餐模型

想象有 limit 个盘子(并发限制),和 一锅包子(任务列表)。

  1. 抢盘子:一开始先派 limit 个人,每人拿个盘子去夹包子(启动 limit 个任务)。
  2. 占位吃:每人手里只能拿 1 个包子,吃完才能拿下一个。
  3. 接力跑:一旦有人吃完了(finally),他把盘子空出来了,立刻再去锅里夹下一个包子(run() 递归),直到锅空了为止。
  4. 最后买单:有一个服务员在旁边计数,只有当吃掉的包子数 === 包子总数时,才通知老板结账(resolve)。

二、 解题公式:1个容器,3个变量,4步逻辑

1 个容器

外层必须包裹 return new Promise((resolve) => { ... })。

3 个变量
  1. index发牌员。指向下一个要领取的任务(只增不减)。
  2. finished计数员。记录已经做完的任务数(用来判断何时 resolve)。
  3. results记账本。用来存结果。
4 步逻辑 (在 run 函数里)
  1. 哨兵检查:if (index >= len) return(没包子了,收工)。

  2. 领任务:i = index++(闭包保存当前下标,同时指针后移)。

  3. 做任务:task().then(存结果).finally(下一步)。

  4. 递归与结束

    • finally 里先 finished++。

    • 判断:如果 finished === len 

      →→
      

       resolve。

    • 否则:run()(继续领下一个)。

const asyncPool = (limit, tasks) => {
  // 1. 容器:Promise
  return new Promise((resolve) => {
    
    // 2. 变量:结果数组、发牌指针、完成计数
    const results = [];
    let index = 0;
    let finished = 0;

    // 边界:空数组直接返回
    if (tasks.length === 0) return resolve([]);

    // 3. 核心:Worker 函数
    const run = () => {
      // 3.1 哨兵:没任务了就退下
      if (index >= tasks.length) return;

      // 3.2 领任务:闭包保存当前下标 i
      const i = index; 
      index++; // 指针后移

      // 3.3 执行
      tasks[i]()
        .then(res => results[i] = res)
        .catch(err => results[i] = err)
        .finally(() => {
          // 3.4 结算与接力
          finished++; // 完成一个
          
          if (finished === tasks.length) {
            resolve(results); // 全做完了,交卷
          } else {
            run(); // 没做完,当前线程空闲,自动去领下一个
          }
        });
    };

    // 4. 启动:先由 limit 个工人把线程占满
    for (let i = 0; i < Math.min(limit, tasks.length); i++) {
      run();
    }
  });
};

Q30:退避重试

const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))

const retryWithBackoff = async (fn, maxRetry = 3, baseDelay = 1000) => {
  let lastError

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

      if (i === maxRetry - 1) {
        throw lastError
      }

      const delay = Math.pow(2, i) * baseDelay
      // 等待一段时间再重试
      await sleep(delay)
    }
  }
}


Q31: 实现promisy

简单来说,promisify 就是一个转换器
它把一个  “老式的、用回调函数接收结果的函数” (比如 Node.js 里的 fs.readFile),变成一个  “现代的、返回 Promise 的函数”

/**
 * @param {Function} originalFn 原生只要接收回调的函数
 * @return {Function} 返回一个返回 Promise 的新函数
 */
function promisify(originalFn) {
    // 1. 返回一个新的函数(高阶函数)
    return function(...args) {
        // 2. 这个新函数必须返回一个 Promise
        return new Promise((resolve, reject) => {
            
            // 3. 我们定义一个“替身回调”,用来替换原本调用者应该传的回调
            const callback = (err, data) => {
                // Node.js 风格:第一个参数是 error
                if (err) {
                    reject(err); // 有错就 reject
                } else {
                    resolve(data); // 没错就 resolve 结果
                }
            };

            // 4. 调用原函数
            // 关键点 A: 拼接参数。原参数 (...args) + 我们的替身回调
            // 关键点 B: 绑定 this。使用 .call 或 .apply 保证原函数的 this 不丢失
            originalFn.apply(this, [...args, callback]);
        });
    };
}

总结

promisify 的实现公式:

闭包包装 + Promise 容器 + 拦截回调 + Apply 调用

你只需要记住:我们在 Promise 内部生成了一个回调函数,把它塞给原函数。当原函数执行完调用这个回调时,我们将结果通过 Promise 的 resolve/reject 弹射出去。

Q32: 实现 Scheduler 类,add(promiseCreator),同时运行任务最多 N 个。

  1. limit (窗口数) :假设银行只有 2 个窗口。
  2. add (取号) :客户来了,取个号。
  3. 直接办理:如果窗口有人空闲(运行任务数 < limit),直接去办理。
  4. 排队:如果窗口都满了,客户就坐在休息区等候(放入 queue 队列)。
  5. 叫号:一旦某个窗口的人办完了(finally),柜员就会对着休息区喊:“下一位!”(从队列取出一个任务继续运行)。
class Schedule {
  constructor(limit) {
    this.limit = limit;
    this.taskList = [];
    this.currentProcess = 0;
  }

  add(promiseTask) {
    return new Promise((resolve, reject) => {
      // 存入队列,等待调度
      this.taskList.push({
        task: promiseTask,
        resolve,
        reject
      });
      // 尝试去跑
      this.run();
    });
  }

  run() {
    // 1. 【门卫检查】:如果满员了,或者队列空了,直接停止,不许往下走
    if (this.currentProcess >= this.limit || this.taskList.length === 0) {
      return;
    }

    // 2. 【取出任务】:通过了门卫检查,才能拿任务
    const currentTask = this.taskList.shift();
    const { task, resolve, reject } = currentTask;

    // 3. 【占用名额】
    this.currentProcess++;

    // 4. 【执行任务】
    task()
      .then(res => resolve(res))  // 成功,通过“遥控器”告诉外面
      .catch(err => reject(err))  // 失败
      .finally(() => {
        // 5. 【释放名额 & 递归】
        this.currentProcess--;
        this.run();
      });
  }
}

五、附录:典型代码片段

建议自己手打一遍,加深记忆。

5.1 经典 async/await + Promise 执行顺序题

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}

async function async2() {
  console.log('async2');
}

console.log('script start');

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

async1();

new Promise(resolve => {
  console.log('promise1');
  resolve();
}).then(() => {
  console.log('promise2');
});

console.log('script end');

输出顺序(务必自己先做,再对照):

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout

使用建议:

  • 面试前按顺序看:

    1. 第一章 + 第二章 → 概念、执行顺序、API 全过一遍;
    2. 第三章 checklist → 自查哪些点还说不完整;
    3. 第四章题库 → 闭卷做完,再看答案;
    4. 最后回到附录代码,再手写几遍。
  • 真正被问到时:

    • 先画出“同步 → 微任务 → 宏任务”的大致顺序;
    • 逐行模拟执行,心态放松,不急着一口气报答案。

首先模拟实现一个有问题的版本


function Bromise(executor) {
    var onResolve_ = null
    var onReject_ = null
     //模拟实现resolve和then,暂不支持rejcet
    this.then = function (onResolve, onReject) {
        onResolve_ = onResolve
    };
    function resolve(value) {
          //setTimeout(()=>{
            onResolve_(value)
           // },0)
    }
    executor(resolve, null);
}

开始执行

function executor(resolve, reject) {
    resolve(100)
}
//将Promise改成我们自己的Bromsie
let demo = new Bromise(executor)

function onResolve(value){
    console.log(value)
}
demo.then(onResolve)

上面代码的问题,执行就会报错 Uncaught TypeError: onResolve_ is not a function

执行到这里 let demo = new Bromise(executor) 其实就报错了,因为then 函数的回调还没绑定,执行的时候onResolve_还没有

易错点

这是一个非常关键的问题!很多开发者在刚开始写 async/await 时都会在这里栽跟头。

简单直接的回答是: map 可以用 await,但效果和 for 循环完全不同!

  • for...of + await = 串行(一个接一个,排队)。
  • map + await = 并发(一起上,不排队)。

而且,如果你单独用 map 而不配合 Promise.all,你会得到一个全是 Promise 的数组,而不是结果。


1. 直观演示:发生了什么?

假设我们要烤 3 个面包,每个耗时 1 秒。

如果用 for 循环 (串行)
// 耗时:3秒
for (const bread of breads) {
  await bake(bread); // 烤完这个,才进下一次循环
}
如果用 map (并发)
// 耗时:1秒 (因为是同时烤的)
const results = breads.map(async (bread) => {
  return await bake(bread);
});

console.log(results); 
// 输出:[Promise { <pending> }, Promise { <pending> }, Promise { <pending> }]
// 注意:这里拿到的不是面包,而是“面包兑换券”!

2. 为什么 map 拿不到直接结果?

map 是一个同步方法。它的工作原理是:

“我看一眼数组第一个元素,执行回调函数,把返回值扔进新数组;立刻看第二个元素,执行回调,扔进新数组……”

当你的回调函数是 async 时:

  1. async 函数被调用,立刻返回一个 Promise(状态是 pending)。
  2. map 收到了这个 Promise,把它放进新数组。
  3. map 继续处理下一个……

所以,map 执行完那一瞬间,你得到的是一个 Promise 数组

3. 正确用法:搭配 Promise.all

既然 map 返回的是一堆 Promise,我们需要一个东西来等待这一堆 Promise 全部变成结果。这就是 Promise.all 的最佳使用场景。

这是前端开发中最常用的并发写法(背下来):

async function bakeAll() {
  const breads = [1, 2, 3];

  // 1. map 负责把“原料”变成“正在执行的任务(Promise)”
  const promises = breads.map(async (bread) => {
    const res = await bake(bread);
    return res;
  });

  // promises 现在是 [Promise, Promise, Promise]

  // 2. Promise.all 负责等待它们全部完成
  const results = await Promise.all(promises);

  return results; // [面包1, 面包2, 面包3]
}

总结与对比

方法代码写法特点适用场景
for...offor (const x of arr) { await fn(x) }串行 (阻塞)前一个必须做完才能做下一个(如:按顺序爬虫、数据库事务)。
forEacharr.forEach(async x => await fn(x))失控 (无法等待)千万别用! 主程序不会等你,你也拿不到结果。
mapPromise.all(arr.map(async x => ...))并行 (并发)互不依赖的任务(如:批量上传图片、批量请求接口)。

结论

  • 如果你想并发(同时跑),请用 Promise.all + map
  • 如果你想串行(排队跑),请用 for 循环。
  • 永远不要指望 map 自己能 await 出结果,它只会给你一堆 Promise。