主题
实现 catch 方法与实现 Promise.resolve(Day18 3/3)
本页对应 Day18-自定义Promise,共 5 段视频,约 1 小时 7 分。
学习目标
- 能够基于
then方法实现catch,并解释其等价于then(undefined, onRejected),通过连续失败传递示例验证异常穿透效果。 - 能够实现静态方法
Promise.resolve,并分别验证不传参/非 Promise 值、Promise 对象、thenable 对象及异步改变状态时的返回结果。 - 能够实现静态方法
Promise.reject,验证其始终返回失败状态 Promise,且传入值直接作为失败原因。 - 能够实现静态方法
Promise.all,验证其支持可迭代对象、按参数顺序收集成功结果、首个失败即失败、非可迭代对象返回失败,并正确处理稀疏结果数组。 - 能够实现静态方法
Promise.race,验证其返回首个完成成员的状态和结果,并能捕获遍历可迭代对象时的异常并转为失败状态。
知识点
catch 的语义定位:只处理失败
为什么需要:在 Promise 链式调用中,then 可以同时接收成功和失败两个回调,但实际开发里经常只关心成功路径,失败路径希望集中到链尾处理。如果每个 then 都写第二个回调,代码会重复、分散。catch 就是给失败处理提供一个专用入口。
怎么想:笔记里写得很直接:“catch 只传入失败状态的回调,相当于 then(undefined, 回调函数)。”也就是说,catch(onRejected) 并不需要重新实现一套状态流转逻辑,它只是把失败回调转交给已经实现好的 then。成功状态不归 catch 管,失败状态才由它接收。
怎么做:在 Promise 类的原型上增加 catch 方法,参数是 onRejected,返回值是 this.then(undefined, onRejected)。这样既复用了 then 的链式能力,又保证 catch 返回的仍然是一个 Promise 实例。
易错点:不要写成 this.then(onRejected),因为 then 的第一个参数是成功回调,那样会把失败回调错放到成功路径;第一个参数要显式传 undefined,表示不处理成功情况。也不要忘记 return,否则链式调用会断掉。
catch 的实现:站在 then 的肩膀上
为什么需要:then 已经处理了成功/失败回调的注册、状态传递和返回值包装。catch 如果另起炉灶,会重复实现状态机,既容易出错,也破坏一致性。
怎么想:转写稿里反复强调:“catch 就相当于第一个参数不传,只传第二个参数”“第一个不传就不做处理,只处理失败的情况”。因此实现时不需要复杂判断,直接把 onRejected 放到 then 的第二个参数位置即可。
怎么做:保留本节笔记代码:
js
(window => {
class Promise {
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
* @param {Function} onRejected 实例状态失败执行的回调
*/
catch(onRejected) {
return this.then(undefined, onRejected);
}
}
window.Promise = Promise;
})(window || global);这段代码的核心只有一行:return this.then(undefined, onRejected);。this 是当前 Promise 实例,then 返回新的 Promise,所以 catch 也天然支持链式调用。
易错点:catch 不是直接执行 onRejected,而是把它交给 then,由 then 在失败状态到来时调用;如果把 catch 写成同步调用回调,就失去了 Promise 的异步状态传递。另一个易错点是误以为 catch 只能放在链尾,实际上它返回 Promise,可以继续接 then,但本节只讨论失败回调的转交。
链式调试:失败状态如何“穿透”到 catch
为什么需要:只看一行实现容易觉得“太简单”,需要验证失败状态在多个 then 没有写第二回调时,是否会继续向下传递,最终被 catch 接住。
怎么想:转写稿的调试思路是:先把 P1 状态改成成功,链上几个 then 都不指定第二个回调,最后写 catch。此时成功状态会沿链向下传递,catch 不执行,控制台输出类似“成功2、成功3”。再把 P1 改成失败,前面的 then 都没有失败回调,失败状态不会被它们处理,而是继续向下传递,最后进入 catch。
怎么做:调试时在 catch 里打印失败原因,例如 console.log('最终失败', reason)。转写稿中验证到:P1 失败后,前面几个 then 处理不了,失败状态继续传递到 catch,最终打印失败,并且 reason 就是 reject 传入的值。这里的“异常穿透”指的是失败状态沿链继续传递,直到遇到能处理失败的回调。
易错点:不要把“失败状态传递”误解成 catch 能捕获所有同步异常;本节实现只是把失败回调交给 then。调试时还要注意:如果中途某个 then 写了失败回调,失败就可能在那里被处理,后面的 catch 不一定再收到原失败原因。另一个易错点是只验证成功路径就下结论,必须把 P1 改成失败,观察 catch 是否真的执行,并检查打印的 reason 是否与 reject 参数一致。
先明确 Promise.resolve 的四种输入情况
为什么需要:Promise.resolve() 是 Promise 类上的静态方法,用来“根据传入值快速得到一个 Promise 实例”。它常用于把普通值、Promise、thenable 统一包装成 Promise,方便后续 .then() 链式处理。
怎么想:先按参数类型分情况。笔记给出的规则是:
text
情况一:没有参数,返回成功状态Promise实例,result是undefined
情况二:参数是非Promise,返回成功状态Promise实例,result是参数
情况三:参数是Promise实例,参数就是返回值
情况四:参数是thenable对象,返回Promise实例,状态由thenable对象中的then方法决定注意笔记里第二个“情况一”实际应理解为“情况二”:参数不是 Promise 时,返回成功状态的 Promise,并把参数作为 result。
怎么做:方法内部先判断 value instanceof Promise,再判断 typeof value?.then === 'function',最后落到普通值分支。
易错点:不要一上来就访问 value.then。如果 value 是 undefined 或 null,直接写 value.then 会报错;代码里用 value?.then 就是为了避开这个问题。转写稿调试时也提到:不传参时 value 是 undefined,访问 undefined.then 会出错,所以要先处理。
情况一和情况二:普通值直接包装成成功 Promise
为什么需要:大多数调用 Promise.resolve(100)、Promise.resolve() 的场景,本质都是希望立刻得到一个成功状态的 Promise,result 分别是 100 和 undefined。
怎么想:这两种情况不需要特殊判断,直接 new Promise,在执行器里调用 resolve(value) 即可。resolve 会把 Promise 状态改为成功,并把 value 作为 result。
怎么做:
js
return new Promise((resolve, reject) => {
resolve(value);
});易错点:不要手动改状态或返回普通值;Promise.resolve 的返回值必须是一个 Promise 实例。转写稿演示中传 100 时,状态成功、result 是 100;不传参时状态成功、result 是 undefined。
情况三:参数已经是 Promise,直接返回它
为什么需要:如果传入的已经是 Promise 实例,再包一层没有意义,规范行为是直接返回这个实例本身。
怎么想:先判断 value instanceof Promise。成立就 return value。
怎么做:
js
if (value instanceof Promise) {
return value;
}易错点:这里判断的是当前自定义的 Promise 类。转写稿演示“传入 P1,返回的就是 P1”,如果 P1 是 pending,返回的也是 pending;P1 后来变成失败,返回的实例也跟着失败。不要误以为会复制出一个新 Promise。
情况四:thenable 对象由它的 then 方法决定状态
为什么需要:有些对象不是真正的 Promise,但带有 then 方法,这种对象叫 thenable。Promise.resolve 要能识别它,并把 resolve、reject 交给它的 then 方法,让 thenable 自己决定最终状态。
怎么想:判断 typeof value?.then === 'function'。成立时不能直接返回 value.then(...),因为 then 的返回值不一定是 Promise;应当新创建一个 Promise,把外层的 resolve、reject 传给 thenable 的 then。
怎么做:
js
return new Promise((resolve, reject) => {
setTimeout(() => {
value.then(resolve, reject);
});
});转写稿里特别调试了“为什么要加 setTimeout”:内置 Promise 对 thenable 的 then 调用是异步的,所以刚输出时可能还是 pending,展开后才会看到 fulfilled 或 rejected。为了和系统行为保持一致,这里把 value.then(resolve, reject) 放进 setTimeout 中异步执行。thenable 内部调用 resolve,外层就成功;调用 reject,外层就失败。
易错点:不要写成同步直接调用,否则时序会和原生不一致;也不要漏掉 typeof ... === 'function',否则普通对象没有 then 会进入错误分支。转写稿还提醒:情况四相对冷门,但既然内置支持,就要按规则补上。
完整实现代码
js
(window => {
class Promise {
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
* @param {Mixed} value 该参数影响返回的Promise
*/
static resolve(value) {
if (value instanceof Promise) {
// 情况三 参数就是Promise对象
return value;
} else if (typeof value?.then === 'function') {
// 情况四 将resolve, reject传递给 value.then方法,由该 thenable 对象决定改为什么状态
return new Promise((resolve, reject) => {
setTimeout(() => {
value.then(resolve, reject);
})
});
} else {
// 情况一 情况二 返回成功状态的Promise对象 value作为result
return new Promise((resolve, reject) => {
resolve(value);
})
}
}
}
window.Promise = Promise;
})(window || global);易错点总结:分支顺序要先判断 Promise 实例,再判断 thenable,最后才是普通值;value?.then 用来防止空值访问;情况四要用 new Promise 包一层并异步调用 then;情况一、二不要忘记把 value 传给 resolve,否则 result 会丢。
先看本节代码与目标
本节要实现的是 Promise.reject()。它和上一节的 Promise.resolve() 一样,都是挂在 Promise 类本身上的静态方法,不是实例方法。笔记给出的实现如下:
js
(window => {
class Promise {
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
* @param {Mixed} value 该参数作为返回值的result
*/
static reject(value) {
return new Promise((resolve, reject) => {
reject(value);
});
}
}
window.Promise = Promise;
})(window || global);这段代码很短,但要把“为什么这样写”讲清楚,需要抓住三个点:静态方法定位、参数直接作为拒绝原因、内部复用构造函数。
为什么需要 Promise.reject()
为什么需要: 实际开发中经常需要“直接得到一个失败状态的 Promise”。例如某个分支已经确定是错误,希望统一走 .catch();或者测试失败链路时,需要快速造一个 rejected 实例。若每次都写 new Promise((resolve, reject) => reject(value)),代码重复且语义不直观。Promise.reject(value) 就是快捷入口。
怎么想: 它不处理成功分支,也不像 Promise.resolve() 那样区分普通值、Promise、thenable 等情况。转写稿里说得很直接:reject “肯定是得到一个失败状态的”,参数是什么,result 就是什么,“他也不挑,哪怕你的参数还是一个 Promise 对象呢?无所谓,只不过作为 result,不会有任何影响”。
怎么做: 把它定义成 static reject(value),返回一个新的 Promise 实例,并在执行器里调用 reject(value)。
易错点: 不要写成实例方法;不要误以为它会像 resolve 那样递归展开 Promise 或 thenable。reject 的参数就是拒绝原因,原样保留。
怎么想:静态方法要挂在类本身
为什么需要: Promise.reject() 的调用形式是 Promise.reject(250),不是 p.reject(250)。因此它必须定义在类本身上,也就是使用 static。
怎么想: 笔记注释明确写了“该方法添加到类本身(静态方法)”。这说明它和 Promise.resolve() 是同一层级的能力:由类直接调用,返回一个新的 Promise 实例。
怎么做: 在 class Promise 内部写 static reject(value) { ... },内部仍然使用 new Promise(...) 创建实例。
易错点: 若漏掉 static,方法会变成实例方法,Promise.reject 就不存在;若在方法里 return this 或返回旧实例,也不符合“返回新的失败状态 Promise 实例”的要求。
怎么做:复用构造函数并调用 reject
为什么需要: Promise.reject() 不应该重新实现一套状态机,而应复用已经写好的 Promise 构造函数。构造函数内部已经提供了 resolve 和 reject 两个执行器参数。
怎么想: 要得到失败状态,只需要在执行器里调用第二个参数 reject。参数 value 直接传进去,作为失败原因。转写稿中对应说法是:“这边调用的一定是谁啊?reject。那这个 result 呢?是不是它的参数?”
怎么做: 写成:
js
static reject(value) {
return new Promise((resolve, reject) => {
reject(value);
});
}这里 resolve 虽然没用到,但执行器函数签名需要保留,因为第二个参数才是 reject。也可以写成 (_, reject),但笔记保留 resolve 更直观。
易错点: 不要在执行器里调用 resolve(value),否则会得到成功状态;不要忘记 return new Promise(...),否则 Promise.reject() 返回 undefined;参数名不要和内部 reject 混淆。
调试过程与行为验证
为什么需要: 方法写完必须验证返回值状态和拒绝原因,否则容易把“失败状态”和“失败原因”搞混。
怎么想: 转写稿中通过 index.html 观察:调用后应看到失败状态;传入 250,失败原因就应是 250;“穿什么都是嗯”,即传什么就作为什么结果。还特别提到,即使参数本身是一个 Promise 对象,也只是把它当作 result,不会产生额外影响。
怎么做: 可写类似验证:
js
Promise.reject(250).catch(reason => {
console.log(reason); // 250
});观察状态应为 rejected,原因是传入值。若传入 Promise 对象,拒绝原因仍是该对象本身,而不是等待它完成后的结果。
易错点: 不要用 .then 的成功回调验证;不要以为 reject 会像 resolve 那样“展开”参数;测试时注意区分状态 rejected 与原因 reason。
易错点集中提醒
为什么需要: reject 实现短,反而容易凭直觉写错。
怎么想: 记住一句话:Promise.reject(value) 返回“新的、已失败的 Promise”,value 原样作为拒绝原因。
怎么做: 保持静态方法、返回新实例、执行器调用 reject(value) 三件事即可。
易错点: 第一,漏写 static;第二,误调 resolve;第三,误以为会处理 thenable;第四,忘记返回新 Promise;第五,把笔记里的 result 与状态混淆。转写稿说它“好简单完事了”,但前提是前面的 Promise 构造函数和状态机已经正确。
为什么需要 Promise.all()
实际开发里经常要“并发发起多个异步任务,等它们全部完成后再统一处理”。比如页面初始化要同时请求用户信息、菜单权限、配置项,三个请求都成功后再渲染;只要有一个失败,就整体走失败逻辑。Promise.all() 就是解决这个问题的静态方法。
它有两个核心规则:参数是可遍历对象时,所有成员都成功,返回的 Promise 才是成功,结果是按顺序保存每个成员结果的数组;只要有一个成员失败,返回的 Promise 立即失败,结果是第一个失败成员的原因。参数不是可遍历对象时,直接返回失败状态的 Promise。
先想清楚参数和返回值
Promise.all(items) 的 items 不一定是数组,而是“可遍历对象”(Iterable),例如数组、字符串、Set 等。成员也不一定都是 Promise,普通值也要能处理,所以内部要用 Promise.resolve(item) 统一转成 Promise。
返回值一定是一个新的 Promise。成功时 resolve(resArr),失败时 reject(reason)。这里最容易想到但最容易写错的是“结果顺序”:不能谁先完成就 push 谁,否则结果顺序会乱;必须按原始成员的索引写入结果数组。
保留本节代码
js
(window => {
class Promise {
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例,该方法添加到类本身(静态方法)
* @param {Iterable} items 里面的成员都是Promise对象,不是Promise也会用Promise.resolve()变为Promise对象
* @return {Promise} 所有成员都成功状态成功
*/
static all(items) {
return new Promise((resolve, reject) => {
if (typeof items[Symbol.iterator] === 'function') {
// 将参数 items 转为数组
const itemsArr = Array.from(items);
// 记录每个成员成功之后的结果
const resArr = Array(itemsArr.length);
// 遍历所有成员
itemsArr.forEach((item, index) => {
const p = Promise.resolve(item); // 处理每个成员
p
.then(res => {
resArr[index] = res;
// 如果resArr填充满了,说明所有的成员都完成了
if ((resArr.filter(()=>true)).length === itemsArr.length) {
resolve(resArr);
}
})
.catch(reason => {
reject(reason);
});
})
} else {
// 说明参数items不是可遍历对象
reject('argument must is a iterable');
}
})
}
}
window.Promise = Promise;
})(window || global);逐段拆解实现思路
先判断 items[Symbol.iterator] 是否为函数,这是判断“可遍历对象”的关键。转写稿里提到,for...of 只能遍历可遍历对象,不是可遍历对象会直接报错,所以也可以用 try/catch 包住 for...of。本节代码选择显式判断 Symbol.iterator。
接着用 Array.from(items) 转成数组。为什么转?因为后面要拿索引,而直接 for...of 只能拿值,拿不到索引;转成数组后可以用 forEach((item, index) => {}),索引稳定。
然后创建 resArr = Array(itemsArr.length),长度先定好,结果按 resArr[index] = res 写入。转写稿里特别强调:不能 push,因为异步完成顺序不可控,push 会破坏结果顺序。按索引写,才能保证最终数组和传入顺序一致。
每个成员先 Promise.resolve(item),普通值也能变成成功 Promise。成功回调里写结果,并用 resArr.filter(() => true).length === itemsArr.length 判断是否全部填满。这里用 filter 是为了过滤稀疏数组的空位;不能简单判断 includes(undefined),因为某个成员的结果本身可能就是 undefined。失败回调直接 reject(reason),第一个失败原因就是最终原因。
易错点与调试提醒
第一,参数不是可遍历对象时要返回失败 Promise,而不是直接抛错。第二,items 为 null 或 undefined 时,直接访问 items[Symbol.iterator] 会报错,实际工程里应先判空。第三,结果顺序必须按索引保存,不能按完成先后 push。第四,判断“全部完成”不能靠结果值是否 undefined,要用“非空位数量”判断。第五,失败后 Promise 状态已经确定,后续成功回调再 resolve 也不会改变结果。转写稿中的验证也说明:即使某个成员延迟 2 秒完成,最终结果仍按传入顺序排列;传入字符串这类可遍历对象也能得到对应结果;传入普通数字则走失败分支。
为什么需要 Promise.race()
Promise.all() 关心的是“所有成员都成功才成功”,而 Promise.race() 关心的是“谁先完成就用谁的结果”。它适合做超时控制、多个数据源竞速、只取最先返回结果的场景。本节要实现的规则来自笔记:
1. 传入可遍历对象,一个完成,就最终完成
2. 参数不是可遍历对象,返回失败状态的promise注意这里的“完成”既可能是成功,也可能是失败:第一个改变状态的成员,会把外层 Promise 的状态定下来。
先明确规则:谁先调用,谁生效
race 返回一个新的 Promise。遍历传入的可迭代对象,对每个成员都挂上成功和失败回调;哪个成员先执行回调,外层 Promise 就立刻变成对应状态。转写稿里反复强调“谁先执行谁就赢了”,因为 Promise 状态一旦确定就不能再改,后面的成员再完成也不会影响最终结果。
这里还要处理成员不是 Promise 的情况。比如成员是数字、字符串,不能直接假定它有 then,所以统一用 Promise.resolve(item) 包一层,把它变成 Promise 再处理。
怎么想:把每个成员的回调接到外层 Promise 上
外层 new Promise((resolve, reject) => { ... }) 里的 resolve、reject 就是最终开关。遍历时对每个成员执行:
js
const p = Promise.resolve(item);
p.then(resolve, reject);这样每个成员成功时会调用外层 resolve,失败时会调用外层 reject。谁先完成,谁就先调用外层开关,race 的结果就是谁。这个过程不需要记录顺序,也不需要像 Promise.all() 那样维护结果数组,所以实现比 all 简单。
如果参数不是可迭代对象,for...of 会直接报错。转写稿中的调试思路是:用 try...catch 包住遍历,捕获异常后调用 reject(error),让返回的 Promise 进入失败状态。笔记代码则采用显式判断:
js
if (typeof items[Symbol.iterator] === 'function') {
// 遍历 items
} else {
reject('argument must is a iterable');
}两种思路都在表达同一件事:不能遍历,就让返回的 Promise 失败。
怎么做:保留本节代码
js
(window => {
class Promise {
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例,该方法添加到类本身(静态方法)
* @param {Iterable} items 里面的成员都是Promise对象,不是Promise也会用Promise.resolve()变为Promise对象
* @return {Promsie}
*/
static race(items) {
return new Promise((resolve, reject) => {
if (typeof items[Symbol.iterator] === 'function') {
// 遍历 items
for (let item of items) {
const p = Promise.resolve(item);
p.then(resolve, reject)
}
} else {
reject('argument must is a iterable');
}
});
}
}
window.Promise = Promise;
})(window || global);这段代码的关键点有三个:
static race(items):race是类本身的静态方法,通过Promise.race()调用。typeof items[Symbol.iterator] === 'function':判断参数是否可迭代;不可迭代时直接reject。Promise.resolve(item).then(resolve, reject):把每个成员的成功/失败都接到外层 Promise 上,谁先完成谁生效。
调试过程:先完成者决定结果
转写稿中验证时,把 Promise.race() 的参数换成几个 Promise,其中让某个成员最先改变状态,结果就取那个成员。比如前面的成员先成功,后面即使还有延迟成员,也不会再影响最终结果。若第一个完成的是失败状态,那么外层 Promise 也失败,并把失败原因传出去。
这里容易和 Promise.all() 混淆:all 要等所有成员都成功,且要按顺序收集结果;race 不需要等全部,也不需要记顺序,所以可以直接 for...of 遍历。
易错点
第一,不能直接对成员调用 item.then(...),因为成员可能不是 Promise,应先 Promise.resolve(item)。第二,参数不是可迭代对象时,不能默默成功;应让返回的 Promise 失败。第三,race 是“第一个完成”,不是“第一个成功”,失败也会抢先决定结果。第四,then(resolve, reject) 中传入的是外层 Promise 的开关,不要误写成新的回调函数,否则结果传不出去。第五,状态一旦确定就不会再变,所以后续成员完成不会覆盖先完成的结果。
课堂画面





代码示例
示例一:catch / resolve / reject(含异常穿透与 resolve 四种情况)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>11-13 catch / resolve / reject</title>
</head>
<body>
<pre id="log"></pre>
<script>
const log = (...args) =>
document.getElementById('log').textContent +=
args.map(v => (typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v))).join(' ') + '\n';
(window => {
// 前置章节的 constructor / then:这里用原生 Promise 做最小委托,保证本段可独立运行
const NativePromise = window.Promise;
class Promise {
constructor(executor) {
this._p = new NativePromise((resolve, reject) => executor(resolve, reject));
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this._p.then(onFulfilled, onRejected).then(resolve, reject);
});
}
// ================= 本节内容 =================
/**
* 给实例传递失败的回调函数,该方法会添加到实例的原型上
* @param {Function} onRejected 实例状态失败执行的回调
*/
catch(onRejected) {
return this.then(undefined, onRejected);
}
/**
* 根据参数返回新的Promise实例,该方法添加到类本身(静态方法)
* @param {Mixed} value 该参数影响返回的Promise
*/
static resolve(value) {
if (value instanceof Promise) {
// 情况三 参数就是Promise对象
return value;
} else if (typeof value?.then === 'function') {
// 情况四 将 resolve, reject 交给 thenable 的 then 决定状态(异步调用)
return new Promise((resolve, reject) => {
setTimeout(() => {
value.then(resolve, reject);
});
});
} else {
// 情况一 情况二 返回成功状态的Promise对象,value 作为 result
return new Promise((resolve) => {
resolve(value);
});
}
}
/**
* 返回新的失败状态的Promise实例,该方法添加到类本身(静态方法)
* @param {Mixed} value 该参数作为返回值的result
*/
static reject(value) {
return new Promise((resolve, reject) => {
reject(value);
});
}
}
window.Promise = Promise;
})(window || global);
// ---------------- 测试 ----------------
// 1. catch 与异常穿透:中间两个 then 都没有写失败回调,失败一路传到最后
new Promise((resolve, reject) => setTimeout(() => reject('error-1'), 100))
.then(res => log('成功2', res))
.then(res => log('成功3', res))
.catch(reason => log('最终失败', reason));
// 2. resolve 四种情况
Promise.resolve().then(res => log('情况一', res)); // undefined
Promise.resolve(100).then(res => log('情况二', res)); // 100
const p2 = new Promise(resolve => setTimeout(() => resolve('P2'), 50));
const p2Back = Promise.resolve(p2);
log('情况三 返回的是不是同一个对象', p2Back === p2); // true
p2Back.then(res => log('情况三', res));
const thenable = { then(res, rej) { setTimeout(() => res('thenable-ok'), 0); } };
Promise.resolve(thenable).then(res => log('情况四', res));
// 3. reject:参数原样作为 result,什么类型都收
Promise.reject(250).catch(reason => log('reject', reason)); // 250
</script>
</body>
</html>关键行与易错点
catch(onRejected)只写一行return this.then(undefined, onRejected):它站在then的肩膀上,第一个参数不传就表示“不处理成功”,只处理失败,返回值同样是 Promise。- 正因为
then不传失败回调时会把失败状态往下传,失败才能一路穿透到最后的catch,这是catch能“接住最后异常”的前提。 resolve的情况三return value:不是new Promise,参数是 Promise 就原样返回,所以Promise.resolve(p) === p为true,测试里专门验证了这一点。- 情况四
value?.then:可选链不能省,resolve()不传参时value是undefined,直接写value.then会抛错(转写稿里就是踩了这个坑)。 - 情况四里包了
setTimeout:thenable的then必须是异步调用,这样状态不会在new的当下就同步改掉,和内置行为保持一致。 reject无论传什么(包括 Promise 对象)都原样作为result,不做任何判断和转换。static方法挂在类本身而不是原型上,调用方式是Promise.resolve(...)而不是实例.resolve(...)。
示例二:Promise.all(保证结果顺序 + 稀疏数组判断)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>14 Promise.all</title>
</head>
<body>
<pre id="log"></pre>
<script>
const log = (...args) =>
document.getElementById('log').textContent +=
args.map(v => (typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v))).join(' ') + '\n';
(window => {
const NativePromise = window.Promise;
class Promise {
constructor(executor) {
this._p = new NativePromise((resolve, reject) => executor(resolve, reject));
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this._p.then(onFulfilled, onRejected).then(resolve, reject);
});
}
catch(onRejected) {
return this.then(undefined, onRejected);
}
static resolve(value) {
if (value instanceof Promise) return value;
if (typeof value?.then === 'function') {
return new Promise((resolve, reject) => {
setTimeout(() => value.then(resolve, reject));
});
}
return new Promise(resolve => resolve(value));
}
/**
* 当传入的所有Promise实例都完成才返回一个Promise实例
* @param {Iterable} items 里面的成员都是Promise对象,不是Promise也会用Promise.resolve()变为Promise对象
* @return {Promise} 所有成员都成功状态才成功
*/
static all(items) {
return new Promise((resolve, reject) => {
if (typeof items[Symbol.iterator] === 'function') {
// 将参数 items 转为数组,这样才能同时拿到 index
const itemsArr = Array.from(items);
// 先按长度占位,后面用 index 往里填,保证顺序
const resArr = Array(itemsArr.length);
itemsArr.forEach((item, index) => {
const p = Promise.resolve(item); // 非 Promise 成员先转成 Promise
p.then(res => {
resArr[index] = res; // 按索引放,谁先完成都不影响顺序
// filter 会跳过空槽,长度相等说明所有位置都填满了
if (resArr.filter(() => true).length === itemsArr.length) {
resolve(resArr);
}
}).catch(reason => {
reject(reason); // 第一个失败就直接让整体失败
});
});
} else {
// 说明参数 items 不是可遍历对象
reject('argument must is a iterable');
}
});
}
}
window.Promise = Promise;
})(window || global);
// ---------------- 测试 ----------------
const p1 = Promise.resolve(100);
const p2 = Promise.resolve(200);
const p3 = new Promise(resolve => setTimeout(() => resolve(300), 800)); // 最慢的一个
const p4 = 400; // 非 Promise 成员
const p5 = 'hello'; // 非 Promise 成员
Promise.all([p1, p2, p3, p4, p5])
.then(res => log('all 成功', res)) // [100,200,300,400,"hello"],p3 最慢但仍在第 3 位
.catch(reason => log('all 失败', reason));
// 有一个成员失败,整体失败,result 是第一个失败成员的 reason
Promise.all([Promise.resolve(1), Promise.reject('出错了'), Promise.reject('第二个错')])
.then(res => log('不该走到这里', res))
.catch(reason => log('all 失败', reason)); // 出错了
// 参数不是可遍历对象
Promise.all(250).catch(reason => log('all 参数错误', reason));
// 字符串也是可遍历对象,每个字符都会被 resolve
Promise.all('abc').then(res => log('all 字符串', res)); // ["a","b","c"]
</script>
</body>
</html>关键行与易错点
Array.from(items)这一步不能省:for...of遍历可迭代对象拿不到索引(Set、Map 根本没有索引),而Promise.all必须按原顺序返回结果,所以先转成数组再forEach。resArr[index] = res而不是resArr.push(res):push 是按完成先后顺序存的,快的先完成会排到前面,顺序就乱了;按索引写才是对的。- 判断“全部完成”用的是
resArr.filter(() => true).length === itemsArr.length:Array(n)是稀疏数组,filter会跳过空槽,所以过滤后长度就是已填数量;不能简单用resArr.length,因为它一开始就等于 n 了。 - 不要用“是否包含
undefined”来判断是否填满:成员的result本身就可能是undefined,会误判。 .catch(reason => reject(reason))里没有break:失败后继续调用reject也没关系,Promise 状态一旦改变就不可再改。- 参数校验用
typeof items[Symbol.iterator] === 'function',不是Array.isArray,因为要求支持所有可迭代对象。 - 已知短板:传入空数组时
forEach一次都不执行,永远不会resolve(会一直 pending),这是这份实现的边界情况,实际使用时可以补一个if (!itemsArr.length) resolve([])。
示例三:Promise.race(谁先完成谁说了算)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>15 Promise.race</title>
</head>
<body>
<pre id="log"></pre>
<script>
const log = (...args) =>
document.getElementById('log').textContent +=
args.map(v => (typeof v === 'object' && v !== null ? JSON.stringify(v) : String(v))).join(' ') + '\n';
(window => {
const NativePromise = window.Promise;
class Promise {
constructor(executor) {
this._p = new NativePromise((resolve, reject) => executor(resolve, reject));
}
then(onFulfilled, onRejected) {
return new Promise((resolve, reject) => {
this._p.then(onFulfilled, onRejected).then(resolve, reject);
});
}
static resolve(value) {
if (value instanceof Promise) return value;
if (typeof value?.then === 'function') {
return new Promise((resolve, reject) => {
setTimeout(() => value.then(resolve, reject));
});
}
return new Promise(resolve => resolve(value));
}
/**
* 传入的多个Promise实例只要一个先完成就返回一个Promise实例
* @param {Iterable} items 里面的成员都是Promise对象,不是Promise也会用Promise.resolve()变为Promise对象
* @return {Promise}
*/
static race(items) {
return new Promise((resolve, reject) => {
if (typeof items[Symbol.iterator] === 'function') {
for (let item of items) {
const p = Promise.resolve(item); // 非 Promise 成员先转换
p.then(resolve, reject); // 谁先触发,就把外层的状态定下来
}
} else {
reject('argument must is a iterable');
}
});
}
}
window.Promise = Promise;
})(window || global);
// ---------------- 测试 ----------------
const slow = new Promise(resolve => setTimeout(() => resolve('slow'), 1000));
const fast = new Promise(resolve => setTimeout(() => resolve('fast'), 100));
Promise.race([slow, fast]).then(res => log('race 胜者', res)); // fast
// 第一个成员直接失败,后面的就不看了
Promise.race([Promise.reject('第一个就失败'), slow])
.then(res => log('不该走到这里', res))
.catch(reason => log('race 失败', reason));
// 参数不是可遍历对象
Promise.race(250).catch(reason => log('race 参数错误', reason));
// 直接传数字,会被 Promise.resolve 转成已完成的 Promise,第一个数字立刻胜出
Promise.race([1, slow]).then(res => log('race 数字胜出', res)); // 1
</script>
</body>
</html>关键行与易错点
- 核心就一句
p.then(resolve, reject):把外层new Promise的resolve / reject直接交给每个成员,谁先触发谁就改变外层状态,后面的调用无效。 - 这里不需要
Array.from、不需要记录索引、也不需要计数:race只关心“第一个”,不存在顺序问题,所以可以直接for...of遍历。 - 与
all的区别要记清:all要等全部完成(且要保序),race一有结果就结束,所以all的复杂度明显更高。 Promise.resolve(item)不能省,成员可能不是 Promise(如1、'hello'),先转换才能调用 `
常见错误
1. catch 把回调塞进 then 的第一个参数
- 现象:失败时没有任何反应,反而在成功时执行了"失败回调";或
catch拿到的参数是 result 而不是 reason。 - 原因:
then的第一个形参是onFulfilled,写成this.then(onRejected)等于把失败处理挂到了成功分支上。 - 修法:
return this.then(undefined, onRejected);,且必须return,否则链式调用断掉。
2. resolve 里直接读 value.then
- 现象:
Promise.resolve()、Promise.resolve(null)直接抛TypeError: Cannot read properties of undefined。 - 原因:判断顺序写成
else if (typeof value.then === 'function'),空值/无参时取属性就崩了。 - 修法:
typeof value?.then === 'function';或把value == null的分支提到最前面。
3. thenable 分支同步调用 then
- 现象:
Promise.resolve(thenable)返回后立刻就能看到成功/失败状态,与原生行为不一致,同一份测试用例时对时错。 - 原因:在 executor 里直接
value.then(resolve, reject),而 executor 是同步执行的。 - 修法:包一层异步(笔记里的
setTimeout),让状态变更发生在当前同步代码之后。
4. all 用 push 收集结果
- 现象:结果数组顺序和入参顺序对不上,晚完成的成员排到了后面。
- 原因:
push按"完成顺序"追加,而all要求按"参数顺序"。 - 修法:先用
Array.from(items)转数组拿到索引,再resArr[index] = res。
5. all 用 resArr.length 判断"全部完成"
- 现象:只要某个成员先完成就提前 resolve,返回的数组是残缺的。
- 原因:稀疏数组给索引 3 赋值后
length立刻变成 4,长度根本代表不了"填满"。 - 修法:
resArr.filter(() => true).length === itemsArr.length;或更省事地维护一个completed计数器,++completed === itemsArr.length才 resolve。
6. race / all 里成员没经过 Promise.resolve 包装
- 现象:传
[1, 2, 3]或混入非 Promise 成员时报p.then is not a function。 - 原因:默认"成员一定是 Promise",直接
item.then(...)。 - 修法:统一
Promise.resolve(item).then(resolve, reject)。
7. 参数校验写成 items[Symbol.iterator] 却不做保护
- 现象:
Promise.all(null)/Promise.all(250)抛同步 TypeError,而不是返回一个 rejected 的 Promise。 - 原因:对
null/undefined取属性直接崩;for...of也在 try 外面。 - 修法:把
Array.from/for...of整个放进try,catch (err) { reject(err) },让非法参数走失败状态。
8. 静态方法漏写 static
- 现象:
Promise.resolve is not a function,但p.resolve又能用。 - 原因:方法挂到了
prototype上,成了实例方法。 - 修法:
static resolve / static reject / static all / static race。
练习
1. 给 all 换掉 filter 判满 把"用 filter(()=>true).length 判断填满"改成计数器版本,要求:不依赖数组长度,成员结果为 undefined 时行为不变。写完用 [1, undefined, 3] 和乱序 setTimeout 两个用例自测。
2. 让 race 的非法参数也走失败分支 当前实现里 Promise.race(250) 会怎样?改成与 all 一致:同步异常被捕获、返回 rejected 状态、reason 为错误对象本身。再补一个 Promise.race(null) 的用例。
3. 实现 Promise.prototype.finally 要求:成功和失败都要执行回调;回调无返回值时原样透传原值/原原因;回调返回 Promise 时要等待它。只能用已有的 then / resolve / reject 实现,一行 then(undefined, fn) 是不够的。
4. 给 resolve 补 thenable 的健壮性 原生规范里 thenable 的 then 只会被取一次,且 then 内部抛错要转成失败状态。按这两条改造 Promise.resolve,并写一个"then 抛异常"的 thenable 验证返回的是 rejected。
5. 对比 all 与 race 的边界 准备 4 组输入:[](空数组)、[非Promise成员]、[pending成员]、[失败成员],分别跑 all 和 race,记录返回值与状态,指出哪一组两者行为必然不同、为什么。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 自定义Promise-实现catch方法 | 05:40 |
| 2 | 自定义Promise-静态方法方法resolve | 15:44 |
| 3 | 自定义Promise-静态方法方法reject | 02:14 |
| 4 | 自定义Promise-静态方法方法all | 38:33 |
| 5 | 自定义Promise-静态方法方法race | 05:44 |
小结
catch是最廉价的一个方法——它没有任何新逻辑,价值全在"复用 then + 把参数放对位置",实现顺序上应当排在 then 之后。resolve的复杂度不在状态怎么改,而在"参数分四种情况"的判定顺序;空值和 thenable 的判定必须放在读.then之前,否则就是崩溃点。reject是全场最简的:不做任何加工、原样透传参数,越"什么都不做"越符合规范。all真正的难点只有两个:顺序(靠索引而非 push)和判满(靠 filter 或计数器而非 length);把这两点想清楚,代码量并不比 race 大多少。- 稀疏数组的
length不可信——它是"最大索引 + 1",不是"已填元素个数",凡是拿length当"完成度"用的地方都是 bug。 for...of拿不到索引、forEach只存在于数组,所以all必须先Array.from转数组;race不需要顺序,直接for...of即可,两个方法的遍历方式差异来自需求本身。- 所有静态方法返回的必然是"新的 Promise 实例",同步抛出(非法参数、成员不是 Promise)一律要转成失败状态,这是与内置行为对齐的统一口径。
- 这一组方法的实现质量高度依赖前面的
then和resolve:then有 bug 会污染全部四个静态方法,resolve有 bug 会让all/race在非 Promise 成员上翻车。