
1. 为什么面试官总爱考手写Promise如果你最近在准备前端面试尤其是中高级岗位那么“手写一个Promise”这道题你大概率是绕不过去的。它就像一道“经典保留曲目”频繁出现在各大公司的技术面中。很多候选人会感到困惑现在前端框架、工程化、性能优化话题那么多为什么面试官还执着于考察这种看似“底层”的、甚至有点“八股”的题目呢这背后其实反映了面试官考察的几个核心能力。首先Promise是异步编程的基石。现代前端开发从数据请求到状态管理再到复杂的UI交互异步操作无处不在。能清晰理解Promise的运作机制意味着你能从根本上把握JavaScript的异步模型这对于排查那些诡异的“Uncaught (in promise) Error”至关重要。其次手写实现是检验理解深度的试金石。调用then、catch方法谁都会但你是否清楚微任务队列Microtask Queue与事件循环Event Loop的配合是否知道resolve一个Promise时thenable对象的处理逻辑手写过程能暴露你对规范细节的掌握程度。最后它考察了将抽象规范转化为具体代码的能力这是工程师基本功的体现。所以准备手写Promise绝不仅仅是为了应付一道面试题。它是一个契机让你系统性地梳理JavaScript的异步世界理解从Callback Hell到Async/Await的演进脉络最终写出更健壮、更易维护的异步代码。接下来我将以一个面试官的视角带你从零开始一步步实现一个符合Promises/A规范的MyPromise并穿插讲解那些面试中高频出现的追问点和实际开发中的避坑经验。2. 构建MyPromise的骨架状态与值一个Promise对象最核心的特征是什么是它的状态State和终值Value。根据规范Promise的状态只能是以下三种之一Pending等待中初始状态既没有被兑现也没有被拒绝。Fulfilled已兑现意味着操作成功完成。Rejected已拒绝意味着操作失败。状态一旦从Pending转变为Fulfilled或Rejected就不可再改变Immutable。这个不可变性是Promise可靠性的基础。同时状态改变时会有一个不可变的**终值value或拒因reason**被传递下去。基于此我们可以先搭建出MyPromise类的构造函数骨架。// 定义三种状态常量避免魔法字符串 const PENDING pending; const FULFILLED fulfilled; const REJECTED rejected; class MyPromise { constructor(executor) { // 初始状态为 Pending this.state PENDING; // 成功后的值 this.value null; // 失败的原因 this.reason null; // 用于存储成功和失败的回调函数队列 // 因为同一个Promise可以多次调用then需要支持多个回调 this.onFulfilledCallbacks []; this.onRejectedCallbacks []; // 定义resolve和reject函数需要绑定this const resolve (value) { // 只有Pending状态可以转换 if (this.state PENDING) { this.state FULFILLED; this.value value; // 状态改变后依次执行所有成功回调 this.onFulfilledCallbacks.forEach(callback callback()); } }; const reject (reason) { if (this.state PENDING) { this.state REJECTED; this.reason reason; // 状态改变后依次执行所有失败回调 this.onRejectedCallbacks.forEach(callback callback()); } }; // 立即执行执行器函数并传入resolve和reject try { executor(resolve, reject); } catch (error) { // 如果执行器同步抛出错误直接reject reject(error); } } }这里有几个关键点需要注意也是面试中常被追问的状态保护resolve和reject函数内部必须检查当前状态是否为PENDING确保状态不可逆。回调队列为什么需要onFulfilledCallbacks和onRejectedCallbacks考虑异步场景new MyPromise(resolve setTimeout(resolve, 1000))。在调用then方法时Promise的状态可能还是PENDING。此时我们需要将传入的回调函数暂存起来等到异步操作完成、状态改变时再触发。这就是发布-订阅模式的典型应用。立即执行与错误捕获executor是同步立即执行的。如果它在执行过程中抛出同步错误例如throw new Error(Oops!)我们需要用try...catch捕获并直接调用reject。这模拟了原生Promise的行为new Promise(() { throw new Error(sync error); })会创建一个被拒绝的Promise。注意这里的resolve和reject函数是用普通函数声明的而不是箭头函数。这是因为我们在构造函数内部定义了它们并需要访问实例的this。如果使用箭头函数this会指向外层上下文可能是全局或undefined导致错误。另一种做法是在调用时绑定this如executor(resolve.bind(this), reject.bind(this))但直接使用普通函数并在其内部通过闭包访问this是更常见的写法。3. 实现then方法链式调用的核心then方法是Promise的灵魂它负责注册回调并返回一个新的Promise从而实现链式调用。它的规范行为比较复杂我们需要拆解实现。3.1 then方法的基本结构then方法接收两个可选参数onFulfilled和onRejected回调。它必须返回一个新的Promise我们称之为promise2。class MyPromise { // ... 之前的构造函数代码 ... then(onFulfilled, onRejected) { // 参数可选如果不是函数则创建一个“透传”函数 onFulfilled typeof onFulfilled function ? onFulfilled : value value; onRejected typeof onRejected function ? onRejected : reason { throw reason; }; // 返回新的Promise const promise2 new MyPromise((resolve, reject) { // 统一封装处理回调的函数 const handleFulfilled () { // 使用queueMicrotask或setTimeout模拟微任务 queueMicrotask(() { try { const x onFulfilled(this.value); // 关键解析promise2与x的关系 resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } }); }; const handleRejected () { queueMicrotask(() { try { const x onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (error) { reject(error); } }); }; // 根据当前状态决定执行逻辑 if (this.state FULFILLED) { handleFulfilled(); } else if (this.state REJECTED) { handleRejected(); } else if (this.state PENDING) { // 状态未定订阅回调 this.onFulfilledCallbacks.push(handleFulfilled); this.onRejectedCallbacks.push(handleRejected); } }); return promise2; } }为什么需要“透传”函数这是Promises/A规范的要求。如果onFulfilled不是函数且promise1是fulfilled状态那么promise2必须以promise1的value被fulfilled。value value这个函数就实现了值的“透传”。同理如果onRejected不是函数且promise1是rejected状态promise2必须以相同的reason被rejectedreason { throw reason; }实现了异常的“冒泡”。这使得错误可以在链的末尾被统一捕获。为什么用queueMicrotaskPromise的回调属于微任务Microtask。为了模拟这一行为我们不能用同步代码直接执行回调。在现代浏览器或Node.js环境中可以使用queueMicrotaskAPI。在不支持的环境下通常用setTimeout(callback, 0)来模拟虽然setTimeout是宏任务在细节上有差异但作为模拟和面试手写是普遍可接受的。这确保了then回调总是在当前同步代码执行完毕后、下一个事件循环之前执行符合“微任务”的语义。3.2 实现resolvePromise处理返回值的复杂性这是整个手写Promise中最复杂、最核心的部分resolvePromise函数用于处理then方法中回调函数的返回值x并决定promise2的最终状态。它需要处理多种情况如果promise2和x指向同一个对象则用TypeError作为原因拒绝promise2避免循环引用。如果x是一个Promise实例则采用它的状态。如果x是一个对象或者函数尝试获取x.then。如果取x.then时抛出错误e则以e为原因拒绝promise2。如果then是一个函数则用x作为this调用它。如果then不是函数则以x为值兑现promise2。如果then是一个函数则需要防止其被多次调用。function resolvePromise(promise2, x, resolve, reject) { // 1. 防止循环引用 if (promise2 x) { return reject(new TypeError(Chaining cycle detected for promise #MyPromise)); } // 标记防止resolve和reject被多次调用 let called false; // 2. 判断x是否为对象或函数可能是一个thenable if ((typeof x object x ! null) || typeof x function) { try { // 3. 获取x.then const then x.then; // 4. 如果then是函数则认为x是一个thenable if (typeof then function) { // 5. 调用then用x作为this then.call( x, // 成功的回调 y { if (called) return; called true; // 递归解析因为y可能还是一个thenable或Promise resolvePromise(promise2, y, resolve, reject); }, // 失败的回调 r { if (called) return; called true; reject(r); } ); } else { // 6. then不是函数直接fulfill resolve(x); } } catch (error) { // 4. 如果取x.then或调用then时抛出错误 if (called) return; called true; reject(error); } } else { // 7. x是普通值直接fulfill resolve(x); } }这个函数实现了Promise解决过程Promise Resolution Procedure。called标志位是面试高频考点它确保了thenable对象的resolve和reject回调只会被调用一次遵循了Promise的状态不可变原则。递归调用resolvePromise(promise2, y, resolve, reject)是为了处理像Promise.resolve().then(() Promise.resolve(42))这样的嵌套情况。4. 完善其他静态方法与实例方法一个完整的Promise实现还包括catch、finally、resolve、reject、all、race等方法。它们都可以基于我们已经实现的then方法来构建。4.1 catch与finally方法catch方法只是then方法的语法糖只处理错误情况。finally方法无论成功失败都会执行并且会将原Promise的结果传递下去。class MyPromise { // ... 之前的代码 ... catch(onRejected) { // 直接调用then第一个参数传null或undefined return this.then(null, onRejected); } finally(onFinally) { // finally回调不接收参数它只是执行一个操作 // 并且返回一个新的Promise其状态和值与原Promise一致除非onFinally返回一个被拒绝的Promise或抛出错误 return this.then( value MyPromise.resolve(onFinally()).then(() value), reason MyPromise.resolve(onFinally()).then(() { throw reason; }) ); } }finally的实现稍微绕一点。它的核心思想是无论原Promise是成功还是失败都先执行onFinally回调。然后如果onFinally执行成功返回一个普通值或成功的Promise就将原Promise的结果值或错误继续传递下去。如果onFinally执行失败抛出错误或返回一个被拒绝的Promise则新的Promise会以这个失败原因被拒绝。这里用MyPromise.resolve包裹onFinally()是为了确保它能处理onFinally返回一个Promise的情况。4.2 静态方法resolve, reject, all, race静态方法是直接挂在类上的不依赖于实例。class MyPromise { // ... 之前的代码 ... static resolve(value) { // 如果value已经是MyPromise实例直接返回 if (value instanceof MyPromise) { return value; } // 如果value是thenable对象则跟随其状态 if (value typeof value.then function) { return new MyPromise(value.then); } // 否则返回一个以该值fulfilled的新Promise return new MyPromise(resolve resolve(value)); } static reject(reason) { // 直接返回一个被拒绝的Promise return new MyPromise((_, reject) reject(reason)); } static all(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } const results []; let completedCount 0; const total promises.length; if (total 0) { return resolve(results); } promises.forEach((promise, index) { // 用MyPromise.resolve包装确保处理的是Promise MyPromise.resolve(promise).then( value { results[index] value; completedCount; if (completedCount total) { resolve(results); } }, // 任何一个失败整个all就失败 reject ); }); }); } static race(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } promises.forEach(promise { // 谁先改变状态race就跟随谁的状态 MyPromise.resolve(promise).then(resolve, reject); }); }); } // 可选的 allSettled 和 any 实现ES2021 static allSettled(promises) { return new MyPromise((resolve) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } const results []; let settledCount 0; const total promises.length; const processResult (index, status, valueOrReason) { results[index] status fulfilled ? { status, value: valueOrReason } : { status, reason: valueOrReason }; settledCount; if (settledCount total) { resolve(results); } }; if (total 0) { return resolve(results); } promises.forEach((promise, index) { MyPromise.resolve(promise).then( value processResult(index, fulfilled, value), reason processResult(index, rejected, reason) ); }); }); } }Promise.all的注意事项它接收一个可迭代对象通常为数组并返回一个新的Promise。所有输入的Promise都成功时它才成功结果数组的顺序与输入顺序一致。只要有一个输入Promise失败它立即失败原因是第一个失败的Promise的原因。实现时我们用MyPromise.resolve包装每个元素这巧妙地将非Promise值转换为Promise并统一了处理逻辑。同时我们需要记录完成的数量并在全部完成时一次性resolve结果数组。Promise.race的实现相对简单它只关心第一个状态落定的Promise无论是成功还是失败并采用其状态和结果。5. 面试高频追问与实战避坑指南手写代码只是第一步面试官往往会根据你的实现进行深度追问或者让你分析一些边缘案例。以下是一些常见的考点和实际开发中的坑。5.1 微任务Microtask与事件循环Event Loop问题为什么Promise回调是微任务它和setTimeout宏任务的执行顺序有什么区别回答要点微任务拥有比宏任务更高的优先级。在一次事件循环中当执行栈清空后会先检查并清空整个微任务队列然后才会从宏任务队列中取出一个任务执行。这保证了Promise链式调用的及时性避免了不必要的UI渲染延迟。在手写时我们用queueMicrotask或setTimeout来模拟但要知道原生Promise使用的是真正的微任务如MutationObserver或process.nextTick。示例console.log(script start); setTimeout(() console.log(setTimeout), 0); Promise.resolve().then(() console.log(promise1)); Promise.resolve().then(() { console.log(promise2); return Promise.resolve(); // 返回一个Promise会产生额外的微任务 }).then(() console.log(promise3)); console.log(script end); // 输出顺序 // script start // script end // promise1 // promise2 // promise3 // setTimeout注意promise2中返回一个Promise这会引入额外的微任务所以promise3会在setTimeout之前打印。5.2 错误处理与“Uncaught (in promise) Error”问题如何理解“Uncaught (in promise) Error”为什么有时候错误没有被catch捕获回答要点这个错误发生在Promise链中某个Promise被拒绝rejected但在其之后没有任何.catch()或第二个参数的.then()来处理这个拒绝。错误会沿着链向下“冒泡”直到被捕获。如果链的末尾都没有捕获在浏览器或Node.js环境中这个未处理的拒绝就会导致警告或错误。避坑实践总是返回Promise链在async函数中确保所有路径都有返回值或抛出错误否则可能返回一个undefined的fulfilled Promise掩盖了真正的错误。在链的末尾使用.catch()这是一个好习惯可以捕获整个链中任何未被处理的错误。区分“抛出错误”和“返回一个被拒绝的Promise”在then的回调中throw new Error()和return Promise.reject(new Error())对于链的下一级是等价的都会被catch捕获。注意catch之后链的继续.catch()也会返回一个新的Promise。如果.catch()回调正常执行没有抛出错误或返回被拒的Promise它会返回一个fulfilled的Promise链可以继续。// 错误未被捕获的例子 new MyPromise((resolve, reject) { reject(new Error(fail)); }).then(() { console.log(success); }); // 控制台会报错Uncaught (in promise) Error: fail // 正确捕获 new MyPromise((resolve, reject) { reject(new Error(fail)); }).then(() { console.log(success); }).catch(error { console.error(Caught:, error); // 这里会捕获到错误 });5.3 Promise.resolve() 与 thenable 对象问题Promise.resolve()的参数可以是一个Promise、一个thenable对象或一个普通值。它是如何工作的回答要点Promise.resolve()的本质是“创建一个已解决的Promise”。如果参数是Promise则直接返回它或采用其状态。如果参数是thenable对象即拥有then方法的对象它会“展开unwrap”这个thenable并返回一个跟随该thenable最终状态的Promise。这个过程就是我们resolvePromise函数中处理then是函数的那段逻辑。这个特性使得Promise可以很好地与其他遵循Promise/A规范的库或自定义异步对象进行互操作。// thenable对象示例 const thenable { then: function(resolve, reject) { setTimeout(() resolve(42), 1000); } }; const p MyPromise.resolve(thenable); p.then(value console.log(value)); // 1秒后打印 425.4 Promise.allSettled 与 Promise.any这是ES2021新增的API了解其语义和手写实现也是加分项。Promise.allSettled等待所有Promise落定无论成功失败返回一个对象数组描述每个Promise的结果。它永远不会被拒绝。Promise.any接收一组Promise只要其中一个fulfill就返回其值。只有当所有Promise都reject时它才reject并返回一个AggregateError。手写any的要点是收集所有错误并在全部失败时统一拒绝。static any(promises) { return new MyPromise((resolve, reject) { if (!Array.isArray(promises)) { return reject(new TypeError(Argument must be an array)); } const errors []; let rejectedCount 0; const total promises.length; if (total 0) { // 根据规范空数组传入any会以一个已拒绝的Promise结束拒绝原因是一个AggregateError return reject(new AggregateError(errors, All promises were rejected)); } promises.forEach((promise, index) { MyPromise.resolve(promise).then( resolve, // 任何一个成功立即resolve reason { errors[index] reason; rejectedCount; if (rejectedCount total) { reject(new AggregateError(errors, All promises were rejected)); } } ); }); }); }6. 从手写到理解Promise的深层价值通过手写一遍Promise我们不仅仅是背下了一段代码更重要的是理解了其设计哲学和解决的核心问题。从回调地狱到声明式异步Promise将嵌套的回调Callback Hell拉平为链式调用使异步代码的流程更加清晰接近于同步代码的书写方式。then方法返回新Promise的特性是链式调用的基础。状态不可变与信任问题Promise的状态一旦确定就不可更改并且决议值value/reason也是不可变的。这解决了传统回调中可能被多次调用调用过早、过晚、次数过多的“信任问题”使得异步操作的结果变得可靠。错误冒泡机制Promise提供了统一的、可预测的错误处理路径。错误可以沿着链向后传递直到被捕获这比在多层回调中分别处理错误要优雅和健壮得多。与Async/Await的关系Async/Await是建立在Promise之上的语法糖。async函数隐式返回一个Promiseawait后面跟的是一个Promise或thenable。理解Promise是理解Async/Await异步编程模型的基石。当你写出const result await someAsyncFunction()时你本质上是在消费一个Promise。在实际面试中当你流畅地写出MyPromise并能清晰地解释上述每一个设计选择、边界案例和底层原理时面试官看到的不仅仅是你对Promise的掌握更是你扎实的JavaScript功底、严谨的编程思维和对异步编程深刻的理解。这才是这道“经典题”背后真正的考察目的。