Skip to content

实现 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);

这段代码的关键点有三个:

  1. static race(items):race 是类本身的静态方法,通过 Promise.race() 调用。
  2. typeof items[Symbol.iterator] === 'function':判断参数是否可迭代;不可迭代时直接 reject。
  3. 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 的开关,不要误写成新的回调函数,否则结果传不出去。第五,状态一旦确定就不会再变,所以后续成员完成不会覆盖先完成的结果。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一: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-静态方法方法resolve15:44
3自定义Promise-静态方法方法reject02:14
4自定义Promise-静态方法方法all38:33
5自定义Promise-静态方法方法race05:44

小结 ​

  1. catch 是最廉价的一个方法——它没有任何新逻辑,价值全在"复用 then + 把参数放对位置",实现顺序上应当排在 then 之后。
  2. resolve 的复杂度不在状态怎么改,而在"参数分四种情况"的判定顺序;空值和 thenable 的判定必须放在读 .then 之前,否则就是崩溃点。
  3. reject 是全场最简的:不做任何加工、原样透传参数,越"什么都不做"越符合规范。
  4. all 真正的难点只有两个:顺序(靠索引而非 push)和判满(靠 filter 或计数器而非 length);把这两点想清楚,代码量并不比 race 大多少。
  5. 稀疏数组的 length 不可信——它是"最大索引 + 1",不是"已填元素个数",凡是拿 length 当"完成度"用的地方都是 bug。
  6. for...of 拿不到索引、forEach 只存在于数组,所以 all 必须先 Array.from 转数组;race 不需要顺序,直接 for...of 即可,两个方法的遍历方式差异来自需求本身。
  7. 所有静态方法返回的必然是"新的 Promise 实例",同步抛出(非法参数、成员不是 Promise)一律要转成失败状态,这是与内置行为对齐的统一口径。
  8. 这一组方法的实现质量高度依赖前面的 then 和 resolve:then 有 bug 会污染全部四个静态方法,resolve 有 bug 会让 all/race 在非 Promise 成员上翻车。