主题
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。
怎么想:看参数分情况。
- 无参数:成功,
result为undefined。 - 参数是非 Promise、非 thenable:成功,参数作为
result。 - 参数是 Promise:直接返回该 Promise,等价于
const p = p1。 - 参数是 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 往往更清楚。
课堂画面





代码示例
示例一: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里调用的第一个 / 第二个参数决定成功还是失败。
- 情况一:无参数 → 成功状态,value 为
Promise.reject非常单纯:永远返回失败状态的 Promise,参数原样作为promiseResult,哪怕参数本身是个 Promise 也不做任何处理。- 易错点:
Promise.resolve(promise)并不是「复制」一个 Promise,而是原样返回;判断时可用===验证。 - 易错点:情况四中自定义
then方法的两个形参(如res、rej)名称任意,但调用第一个 → 成功、调用第二个 → 失败,与new Promise的执行器一致。 - 实际开发中最常用的是情况一和情况二(快速得到一个已成功的 Promise);情况三、四多用于理解语法,框架设计中可能遇到。
常见错误
- 现象:
then只传成功回调,失败时控制台报“未处理”错误。→ 原因:失败状态缺少对应处理回调,链式返回的 Promise 变为失败。→ 修法:在链尾补.catch(reason => {...}),统一处理失败。 - 现象:链式调用中某一步失败后,后续
then的成功回调不执行。→ 原因:失败状态沿 Promise 链向下传递,未遇到catch前不会恢复。→ 修法:把catch放在需要统一兜底的位置,或在该步后立即处理。 - 现象:
finally中试图获取成功值或失败原因,得到undefined。→ 原因:finally的回调不接收结果参数,只做收尾。→ 修法:需要结果时在then/catch中处理,finally只放清理逻辑。 - 现象:
then中发起异步操作但未return,后续步骤顺序错乱。→ 原因:链式调用没有拿到内层 Promise,后续then提前执行。→ 修法:在回调中return新的 Promise,保证链式等待。 - 现象:Node 中连续调用
fs.readFile,输出顺序偶发不是 1、2、3。→ 原因:回调异步完成顺序不确定,调用顺序不等于完成顺序。→ 修法:用 Promise 版读取并then串联,或在上一步完成后再发起下一步。 - 现象:误以为
Promise.reject的参数会变成成功值,或误以为resolve传入 Promise 一定新建对象。→ 原因:混淆类方法与实例方法的状态规则。→ 修法:按方法语义使用;需要统一失败时用reject,需要快速创建成功态时用resolve。
练习
- 写一个 Promise 链:第一步成功、第二步失败,链尾用
catch统一提示“请求失败”,并说明异常如何穿透到catch。 - 改造一段“读取文件1→读取文件2→读取文件3”的 Node 代码,要求用 Promise 链式调用保证严格按 1、2、3 顺序输出。
- 设计一个包含
then、catch、finally的流程,要求finally只负责关闭 loading,不读取成功值或失败原因。 - 判断并说明:
Promise.resolve(100)、Promise.resolve(另一个Promise)、Promise.reject(100)分别得到什么状态的 Promise,结果值/原因是什么。 - 把一段多层嵌套的 Ajax 回调改写成
then链,并在链尾加catch给用户统一错误提示。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | catch和finally对象 | 18:18 |
| 2 | then和catch一起使用解决回调地狱 | 05:03 |
| 3 | Node按顺序读取文件解决回调地狱 | 16:09 |
| 4 | Promise类本身的方法-resolve | 21:13 |
| 5 | Promise类本身的方法-reject | 05:22 |
小结
catch专门处理失败状态,并返回新的 Promise,可继续链式调用。- 链式调用中失败会向下传递,直到被
catch捕获,这就是异常穿透的实用价值。 then链配合catch能显著改善回调地狱的可读性,但异步本质不变。finally不论成功失败都会执行,适合收尾,不接收结果参数。Promise.resolve常用于快速创建 Promise,状态和结果由参数情况决定。Promise.reject返回失败态 Promise,参数直接作为失败原因。- Node 中按顺序读取文件必须显式串联异步步骤,不能依赖
readFile的调用顺序。 - 浏览器对 404 等网络错误会在控制台提示,业务链仍可通过
catch统一兜底。