Skip to content

Promise 实例的方法与Promise 构造函数本身的方(Day16 3/3) ​

本页对应 Day16_Promise,共 5 段视频,约 1 小时 6 分。

学习目标 ​

  • 能够说明 catch 的参数要求与触发条件,并编写 then 与 catch 配合处理成功、失败结果的代码。
  • 能够说明 finally 的回调执行时机与参数特点,并编写包含 finally 的 Promise 收尾代码。
  • 能够使用 then 链式调用改造多层异步请求代码,并解释 catch 捕获前面任一步错误的“异常穿透”原理。
  • 能够使用 Promise 版 fs.readFile 按顺序读取多个文件,并对比回调嵌套写法说明其解决回调地狱的方式。
  • 能够说明 Promise.resolve 在无参数、非 Promise 参数、Promise 参数、thenable 对象四种情况下的返回值状态,并编写对应示例。
  • 能够说明 Promise.reject 的返回值状态及参数作为 promiseResult 的规则,并编写固定返回失败状态的 Promise 示例。

知识点 ​

一、then、catch、finally 的分工 ​

为什么需要:then 可以同时写成功和失败回调,但实际开发中两个回调挤在一起不够清晰;失败如果没人处理,还会抛出错误。catch 用来单独处理失败,finally 用来做收尾。

怎么想:then 返回一个新的 Promise,新 Promise 的状态由回调函数的返回值决定;catch 只处理失败,也返回 Promise;finally 不管成功还是失败都会执行,但它不接收 value 或 reason 参数。

怎么做:先看只给 then 传一个成功回调的情况:

js
const p = new Promise((resolve, reject) => {
  const rndNum = Math.floor(Math.random() * 10) + 1;
  if (rndNum >= 5) resolve(rndNum);
  else reject(rndNum);
});

p.then(value => {
  console.log('成功', value);
}).catch(reason => {
  console.log('失败', reason);
}).finally(() => {
  console.log('finally 一定执行');
});

调试时若只写 p.then(value => console.log('成功', value)),当 p 失败时没有第二个回调处理,就会报错;补上 .catch() 后,失败会进入 catch。finally 放在最后,用于收尾。

易错点:then 只传一个回调时,它只处理成功,不处理失败;catch 返回的也是 Promise;finally 不要试图拿结果参数,它只负责“一定执行”。

二、异常穿透:then 链最后放 catch ​

为什么需要:多个 then 链式调用时,如果每一步都写失败回调会很啰嗦。把失败统一交给最后的 catch,代码更清爽。

怎么想:then 返回新 Promise。若某一步失败且没有失败回调,失败状态会继续向后传递,直到遇到 catch。这就是转写稿里说的“异常穿透”。

怎么做:前端 Ajax 回调地狱可以改成链式:

js
axios.get('/api/list')
  .then(res => {
    console.log('第一步成功', res.data);
    return axios.get('/api/detail');
  })
  .then(res => {
    console.log('第二步成功', res.data);
  })
  .catch(reason => {
    console.log('请求失败,请稍后重试', reason);
  });

调试时若中间某一步地址错了,catch 能统一捕获并给用户提示。转写稿还提醒:浏览器控制台里的 404 报错是浏览器自动提示的,不能靠代码完全规避,但业务失败仍可由 catch 处理。

易错点:catch 要放在链的最后;不要以为 404 控制台红字消失才叫捕获成功;then 中要 return 下一个 Promise,否则链会断。

三、Node 按顺序读取文件 ​

为什么需要:fs.readFile 的回调是异步的,连续写三个读取,谁先读完并不确定,输出顺序可能乱。

怎么想:用 fs.promises.readFile 返回 Promise,再用 then 链式调用:第一个读完再读第二个,第二个读完再读第三个。

怎么做:

js
const fs = require('fs');
const path = require('path');

fs.promises.readFile(path.resolve(__dirname, 'data1.txt'), 'utf8')
  .then(data => {
    console.log('data1.txt 内容:', data);
    return fs.promises.readFile(path.resolve(__dirname, 'data2.txt'), 'utf8');
  })
  .then(data => {
    console.log('data2.txt 内容:', data);
    return fs.promises.readFile(path.resolve(__dirname, 'data3.txt'), 'utf8');
  })
  .then(data => {
    console.log('data3.txt 内容:', data);
  })
  .catch(reason => {
    console.log('读取失败', reason);
  });

易错点:回调写法中“按书写顺序调用”不等于“按顺序读完”;fs.readFile 回调参数是 (err, data);用 fs.promises 后不需要再写回调。

四、Promise.resolve 与 Promise.reject ​

为什么需要:有时需要快速创建一个状态已确定的 Promise,避免每次都 new Promise。

怎么想:Promise.resolve 分情况:无参时成功且结果为 undefined;参数是非 Promise、非 thenable 时成功且参数作为结果;参数是 Promise 时直接返回它;参数是 thenable(带 then 方法的对象)时调用其 then 决定状态。Promise.reject 则永远返回失败状态,参数直接作为失败原因。

怎么做:

js
Promise.resolve(100).then(v => console.log('成功', v));
Promise.reject(100).catch(r => console.log('失败', r));

const p1 = Promise.resolve(1);
console.log(Promise.resolve(p1) === p1); // true

const thenable = {
  then(resolve, reject) {
    Math.random() > 0.5 ? resolve('成功') : reject('失败');
  }
};
Promise.resolve(thenable).then(console.log).catch(console.log);

易错点:resolve 不是“永远成功”,传 Promise 或 thenable 时要看内部状态;reject 的参数即使是 Promise,也只作为失败原因,不会展开。

先分清:实例方法与构造函数方法 ​

本节标题是“Promise 构造函数本身的方法”,但转写稿同时串讲了 then/catch/finally 和 Promise.resolve/reject。先分清对象:then/catch/finally 是 Promise 实例方法,通过 p.then() 调用;Promise.resolve/reject 是构造函数静态方法,通过 Promise.resolve() 调用。
为什么需要:混淆调用位置,后面看返回值、异常穿透就会乱。怎么想:先看方法挂在谁身上。怎么做:实例方法处理某个 Promise 的结果,静态方法快速造 Promise。易错点:catch 不是“捕获所有同步异常”的万能工具,它处理的是 Promise 失败态。

catch:只处理失败,并返回新的 Promise ​

为什么需要:then 写两个回调时,成功和失败混在一起,代码不够清晰;失败处理也常要单独收口。
怎么想:catch 只有一个回调参数,Promise 变为失败态时执行;成功态不执行。它返回一个新的 Promise,状态由回调返回值决定,规则与 then 类似。
怎么做:

js
const p = new Promise((resolve, reject) => {
  const n = Math.random();
  n >= 0.5 ? resolve(n) : reject(n);
});

p.then(value => {
  console.log('成功', value);
}).catch(reason => {
  console.log('失败', reason);
});

调试时如果只写 p.then(value => console.log('成功', value)),失败会报“没有处理失败”的错误;补上 catch 后失败能被接住。
易错点:catch 返回的也是 Promise;不要以为 catch 执行完原 Promise 就变成成功态,原 Promise 状态不会改。

finally:无论成功失败都执行的收尾 ​

为什么需要:加载动画关闭、资源释放等收尾动作,不应区分成功失败。
怎么想:finally 的回调不接受结果参数,不论 Promise 成功还是失败都执行,通常放在 then/catch 后面。
怎么做:

js
p.then(value => {
  console.log('成功', value);
}).catch(reason => {
  console.log('失败', reason);
}).finally(() => {
  console.log('finally 一定执行');
});

易错点:finally 不是用来拿 value/reason 的;不要把它当成 then 的第二个回调。

then + catch 配合:异常穿透与回调地狱 ​

为什么需要:Ajax 多步请求容易写成回调嵌套,失败处理也分散。
怎么想:每个 then 只写成功回调,最后统一 catch。中间某一步失败时,没有失败回调的 then 会返回失败态 Promise,错误一直“穿透”到最后的 catch。
怎么做:

js
axios.get('/api/1')
  .then(res => axios.get('/api/2'))
  .then(res => axios.get('/api/3'))
  .catch(err => {
    console.log('请求失败,请稍后重试');
  });

调试中某一步地址写错,catch 能统一提示“数据获取失败”;浏览器对 404 仍会在控制台报错,这个提示规避不了,但业务错误可由 catch 收口。
易错点:异常穿透依赖中间 then 没有写失败回调;如果中间自己处理了失败,就不会继续穿透。

Node 按顺序读文件:回调嵌套为什么不可靠 ​

为什么需要:验证异步回调不保证按调用顺序完成。
怎么想:fs.readFile 的调用是同步的,但回调是异步的;哪个文件先读完,哪个回调先进入队列,不一定等于书写顺序。
怎么做:回调嵌套能保证顺序:

js
fs.readFile(path1, 'utf8', (err, data1) => {
  console.log(data1);
  fs.readFile(path2, 'utf8', (err, data2) => {
    console.log(data2);
    fs.readFile(path3, 'utf8', (err, data3) => {
      console.log(data3);
    });
  });
});

用 fs.promises.readFile 可改成链式:

js
const fs = require('fs/promises');
const path = require('path');

fs.readFile(path.resolve(__dirname, 'data1.txt'), 'utf8')
  .then(data1 => {
    console.log(data1);
    return fs.readFile(path.resolve(__dirname, 'data2.txt'), 'utf8');
  })
  .then(data2 => {
    console.log(data2);
    return fs.readFile(path.resolve(__dirname, 'data3.txt'), 'utf8');
  })
  .then(data3 => {
    console.log(data3);
  })
  .catch(err => {
    console.log(err);
  });

转写稿中直接并行写三个 readFile,多次运行出现 123、231 等顺序,说明不能靠“看起来按顺序写”保证结果顺序;链式 then 要等上一个 Promise 状态改变后才执行下一个。
易错点:fs.readFile 回调参数是 (err, data),不要写成 (data, err);Promise 版返回 Promise,不要再套回调。

Promise.resolve:快速创建成功态 Promise 的四种参数 ​

为什么需要:有时只想快速得到一个已定状态的 Promise。
怎么想:看参数分情况。

  1. 无参数:成功,result 为 undefined。
  2. 参数是非 Promise、非 thenable:成功,参数作为 result。
  3. 参数是 Promise:直接返回该 Promise,等价于 const p = p1。
  4. 参数是 thenable(有 then 方法的对象):自动调用其 then,由内部 resolve/reject 决定状态。

怎么做:

js
const p1 = Promise.resolve();
const p2 = Promise.resolve(100);
const p3 = Promise.resolve(p1); // p3 与 p1 是同一个 Promise
const thenable = {
  then(resolve, reject) {
    Math.random() >= 0.5 ? resolve('成功') : reject('失败');
  }
};
const p4 = Promise.resolve(thenable);

易错点:情况 3 不是“包一层再返回”,而是直接返回参数;情况 4 的 then 方法名不能乱写。

Promise.reject:快速创建失败态 Promise ​

为什么需要:需要立即得到一个失败态 Promise 做演示或统一错误入口。
怎么想:它比 resolve 单纯:永远返回失败态 Promise,参数原样作为 reason。
怎么做:

js
const p = Promise.reject(100);
p.catch(reason => {
  console.log('失败原因', reason); // 100
});

即使参数本身是 Promise,reject 也把它原样作为失败原因,不会像 resolve 那样展开。
易错点:reject 不分类讨论,永远失败;实际业务中不要为了“看起来高级”滥用,直接 new Promise 往往更清楚。

课堂画面 ​

课堂画面 1

课堂画面 2

课堂画面 3

课堂画面 4

课堂画面 5

代码示例 ​

示例一:then / catch / finally 的基本用法 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Promise 实例方法 then / catch / finally</title>
</head>
<body>
<script>
  // 创建一个随机成功 / 失败的 Promise
  function createP() {
    return new Promise((resolve, reject) => {
      const rndNum = Math.random() * 10;
      if (rndNum >= 5) {
        resolve(rndNum);   // 成功
      } else {
        reject(rndNum);    // 失败
      }
    });
  }

  const p = createP();

  // then 只传一个回调:它只处理「成功」,处理不了「失败」
  p.then(value => {
    console.log('成功', value);
  });

  // 失败没人管 → 控制台会抛 Uncaught (in promise)
  // 解决办法:链式调用 catch
  p.then(value => {
    console.log('成功', value);
  }).catch(reason => {
    console.log('失败', reason);   // 失败时走这里
  }).finally(() => {
    console.log('finally:不论成功还是失败都会执行');  // 没有参数
  });
</script>
</body>
</html>

要点解释与易错点:

  • then 的参数是回调函数,第一个处理成功、第二个处理失败;只写一个时,只处理成功,失败状态下没人处理就会报 Uncaught (in promise)。
  • catch 的参数同样是一个回调函数,只在 Promise 状态为失败(rejected)时执行;它就是 then(null, onRejected) 的语法糖。
  • then 和 catch 都返回一个新的 Promise 对象,状态由回调函数的返回值决定(无返回值 → 成功且值为 undefined;返回 Promise → 跟随该 Promise 的状态;抛错 → 失败)。
  • finally 不论成功失败都执行,回调不接受任何参数,常用于收尾(关闭 loading 等)。
  • 注意:这里链式调用中 catch 捕获的是 then 返回的 Promise 的失败,但因为 then 没写第二个回调,原 p 的失败状态会一路向下传递(异常穿透)。

示例二:then 链式调用解决回调地狱 + 异常穿透 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>then 链式调用解决回调地狱</title>
</head>
<body>
<script>
  // 用 setTimeout 模拟一次异步请求(等价于 axios.get 返回 Promise)
  function request(name, delay, isError = false) {
    return new Promise((resolve, reject) => {
      setTimeout(() => {
        if (isError) {
          reject(new Error(name + ' 请求失败'));
        } else {
          resolve({ name, data: name + ' 的数据' });
        }
      }, delay);
    });
  }

  request('第一步', 1000)
    .then(res => {
      console.log(res.data);
      return request('第二步', 500);        // 必须 return,交给下一个 then
    })
    .then(res => {
      console.log(res.data);
      return request('第三步', 200, true);  // 模拟这一步出错
    })
    .then(res => {
      console.log(res.data);                // 出错后这里不会执行
    })
    .catch(reason => {
      console.log('请求失败,数据获取失败', reason.message); // 统一处理
    })
    .finally(() => {
      console.log('加载完成(无论成功失败)');
    });
</script>
</body>
</html>

要点解释与易错点:

  • 每个 then 中必须 return 下一个 Promise,否则下一个 then 收到的是 undefined,链就断了。
  • 只要任意一步失败,后面所有 then 的成功回调全部跳过,直接穿透到最后的 catch,这就是「异常穿透」。
  • catch 统一处理错误,给用户提示不必说太细(如「数据获取失败」即可)。
  • 易错点:catch 之后如果还要继续链式调用,注意 catch 返回的也是 Promise,其状态由回调返回值决定。
  • 易错点:浏览器对网络层的 404 等错误会在控制台自动打印错误,这部分代码无法屏蔽,但业务层面的错误依然能被 catch 捕获。

示例三:Promise.resolve / Promise.reject 的四种情况 ​

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>Promise.resolve / Promise.reject</title>
</head>
<body>
<script>
  // 情况一:没有参数 —— 返回成功状态的 Promise,value 为 undefined
  const p1 = Promise.resolve();
  p1.then(v => console.log('情况一 成功,value =', v));

  // 情况二:参数是「非 Promise、非 thenable」的值 —— 成功状态,参数即 value
  const p2 = Promise.resolve(100);
  p2.then(v => console.log('情况二 成功,value =', v));

  // 情况三:参数本身是 Promise —— 原样返回该对象
  const pBase = new Promise((resolve, reject) => {
    const rnd = Math.floor(Math.random() * 10);
    rnd >= 5 ? resolve(rnd) : reject(rnd);
  });
  const p3 = Promise.resolve(pBase);
  console.log('情况三:p3 === pBase ?', p3 === pBase);   // true
  p3.then(
    v => console.log('情况三 成功', v),
    r => console.log('情况三 失败', r)
  );

  // 情况四:参数是 thenable 对象(自己写的、带 then 方法的对象)
  const thenable = {
    then(res, rej) {
      const rnd = Math.floor(Math.random() * 10);
      rnd >= 5 ? res(rnd) : rej(rnd);
    }
  };
  const p4 = Promise.resolve(thenable);
  p4.then(
    v => console.log('情况四 成功', v),
    r => console.log('情况四 失败', r)
  );

  // Promise.reject:永远返回失败状态的 Promise,参数原样作为 reason,不分情况
  const p5 = Promise.reject('出错了');
  p5.catch(reason => console.log('reject 捕获到', reason));

  const p6 = Promise.reject(100);
  p6.catch(reason => console.log('参数是数字也原样作为 reason', reason));

  const p7 = Promise.reject(new Promise(() => {}));
  p7.catch(reason => console.log('参数是 Promise 也原样作为 reason', reason));
</script>
</body>
</html>

要点解释与易错点:

  • Promise.resolve 用于快速创建一个 Promise 对象,其状态由参数决定,共四种情况:
    • 情况一:无参数 → 成功状态,value 为 undefined。
    • 情况二:参数是非 Promise 且非 thenable 的值(数字、数组、字符串等)→ 成功状态,参数即 value。
    • 情况三:参数是 Promise → 直接返回该对象本身(p3 === pBase 为 true),所以这种写法通常没有实际意义,直接 new 即可。
    • 情况四:参数是 thenable 对象(具有 then 方法的对象)→ 内部自动调用它的 then,由 then 里调用的第一个 / 第二个参数决定成功还是失败。
  • Promise.reject 非常单纯:永远返回失败状态的 Promise,参数原样作为 promiseResult,哪怕参数本身是个 Promise 也不做任何处理。
  • 易错点:Promise.resolve(promise) 并不是「复制」一个 Promise,而是原样返回;判断时可用 === 验证。
  • 易错点:情况四中自定义 then 方法的两个形参(如 res、rej)名称任意,但调用第一个 → 成功、调用第二个 → 失败,与 new Promise 的执行器一致。
  • 实际开发中最常用的是情况一和情况二(快速得到一个已成功的 Promise);情况三、四多用于理解语法,框架设计中可能遇到。

常见错误 ​

  1. 现象:then 只传成功回调,失败时控制台报“未处理”错误。→ 原因:失败状态缺少对应处理回调,链式返回的 Promise 变为失败。→ 修法:在链尾补 .catch(reason => {...}),统一处理失败。
  2. 现象:链式调用中某一步失败后,后续 then 的成功回调不执行。→ 原因:失败状态沿 Promise 链向下传递,未遇到 catch 前不会恢复。→ 修法:把 catch 放在需要统一兜底的位置,或在该步后立即处理。
  3. 现象:finally 中试图获取成功值或失败原因,得到 undefined。→ 原因:finally 的回调不接收结果参数,只做收尾。→ 修法:需要结果时在 then/catch 中处理,finally 只放清理逻辑。
  4. 现象:then 中发起异步操作但未 return,后续步骤顺序错乱。→ 原因:链式调用没有拿到内层 Promise,后续 then 提前执行。→ 修法:在回调中 return 新的 Promise,保证链式等待。
  5. 现象:Node 中连续调用 fs.readFile,输出顺序偶发不是 1、2、3。→ 原因:回调异步完成顺序不确定,调用顺序不等于完成顺序。→ 修法:用 Promise 版读取并 then 串联,或在上一步完成后再发起下一步。
  6. 现象:误以为 Promise.reject 的参数会变成成功值,或误以为 resolve 传入 Promise 一定新建对象。→ 原因:混淆类方法与实例方法的状态规则。→ 修法:按方法语义使用;需要统一失败时用 reject,需要快速创建成功态时用 resolve。

练习 ​

  1. 写一个 Promise 链:第一步成功、第二步失败,链尾用 catch 统一提示“请求失败”,并说明异常如何穿透到 catch。
  2. 改造一段“读取文件1→读取文件2→读取文件3”的 Node 代码,要求用 Promise 链式调用保证严格按 1、2、3 顺序输出。
  3. 设计一个包含 then、catch、finally 的流程,要求 finally 只负责关闭 loading,不读取成功值或失败原因。
  4. 判断并说明:Promise.resolve(100)、Promise.resolve(另一个Promise)、Promise.reject(100) 分别得到什么状态的 Promise,结果值/原因是什么。
  5. 把一段多层嵌套的 Ajax 回调改写成 then 链,并在链尾加 catch 给用户统一错误提示。

原视频对照 ​

#视频时长
1catch和finally对象18:18
2then和catch一起使用解决回调地狱05:03
3Node按顺序读取文件解决回调地狱16:09
4Promise类本身的方法-resolve21:13
5Promise类本身的方法-reject05:22

小结 ​

  1. catch 专门处理失败状态,并返回新的 Promise,可继续链式调用。
  2. 链式调用中失败会向下传递,直到被 catch 捕获,这就是异常穿透的实用价值。
  3. then 链配合 catch 能显著改善回调地狱的可读性,但异步本质不变。
  4. finally 不论成功失败都会执行,适合收尾,不接收结果参数。
  5. Promise.resolve 常用于快速创建 Promise,状态和结果由参数情况决定。
  6. Promise.reject 返回失败态 Promise,参数直接作为失败原因。
  7. Node 中按顺序读取文件必须显式串联异步步骤,不能依赖 readFile 的调用顺序。
  8. 浏览器对 404 等网络错误会在控制台提示,业务链仍可通过 catch 统一兜底。