一、从错误处理说起:为什么需要 finally()
在 JavaScript 中,我们处理异步操作时,经常用到 Promise。我们会用 then() 处理成功的结果,用 catch() 捕获错误。但总有一些操作,无论成功还是失败,我们都希望它们被执行,比如关闭加载动画、释放资源、清理临时数据。
在没有 finally() 之前,你会在 then() 和 catch() 里重复写同样的代码:
let loading = true;
myPromise
.then(result => {
console.log(result);
loading = false; // 清理操作
})
.catch(error => {
console.error(error);
loading = false; // 同样的清理操作再写一遍
});
这显然不够优雅。代码重复不仅显得臃肿,还容易遗漏。Promise.finally() 的出现就是为了解决这个问题。
二、finally() 的核心作用与机制
finally() 方法的核心思想很简单:它注册一个回调函数,这个回调无论 Promise 最终是变成 fulfilled(成功)还是 rejected(失败),都会被执行。
从机制上看,finally() 会返回一个新的 Promise 对象,这保证了我们可以继续链式调用后续的 then() 或 catch()。它的设计初衷就是用于放置那些“最终必须执行”的清理型任务。
三、基础语法与快速上手
promise.finally(onFinally)
-
onFinally:一个函数,当 Promise 被 settled(敲定,即已成功或已失败)时,该函数会被调用。它不接收任何参数,因此你无法在回调里知道 Promise 的结果是成功还是失败。
返回值:一个新的 Promise,行为与原始 Promise 一致,但它的 finally 处理器已经被设置为传入的 onFinally 函数。
代码号学习编程:一个直观的示例
function queryDatabase() {
return new Promise((resolve, reject) => {
// 模拟数据库查询,随机成功或失败
let success = Math.random() > 0.5;
setTimeout(() => {
if (success) {
resolve("查询成功:获取到用户数据");
} else {
reject("查询失败:数据库连接超时");
}
}, 1000);
});
}
queryDatabase()
.then(result => {
console.log(result); // 处理成功结果
})
.catch(error => {
console.error(error); // 处理错误
})
.finally(() => {
console.log("执行完毕:关闭数据库连接。");
// 在这里进行清理,无论成功还是失败,都会运行
});
运行结果是:
查询成功:获取到用户数据
执行完毕:关闭数据库连接。
或者:
查询失败:数据库连接超时
执行完毕:关闭数据库连接。
你可以看到,“执行完毕:关闭数据库连接”这一行总是会被打印出来。这正是 finally() 的价值所在。
四、深入理解:finally() 与 try...catch...finally 的类比
如果你熟悉同步代码中的 try...catch...finally 结构,那么理解 finally() 就毫无压力。
-
try块中的代码对应 Promise 的执行器函数。 -
catch块对应 Promise 链中的.catch()方法。 -
finally块对应 Promise 链中的.finally()方法。
在同步代码中,finally 块用于释放资源(关闭文件句柄)。在异步的 Promise 世界里,finally() 扮演着相同的角色。
五、实际应用场景与个人见解
在开发中,我经常在以下几个场景使用 finally():
场景一:控制 UI 加载状态
这是最常见的用法。显示加载动画,数据返回后无论成功与否都要隐藏动画。
function fetchUserData() {
// 假设这是 https://www.ebingou.cn/biancheng/images/logo.png 加载的动画标识
showLoadingSpinner(); // 显示加载动画
return fetch('/api/user')
.then(response => response.json())
.then(data => {
// 处理数据,更新页面
console.log("用户数据:", data);
})
.catch(error => {
// 处理错误,显示错误提示
console.error("获取用户数据失败:", error);
})
.finally(() => {
hideLoadingSpinner(); // 隐藏加载动画
});
}
个人经验是,把 hideLoadingSpinner() 放在 finally() 里是安全的做法。你不需要在 then 和 catch 里各写一遍,不仅代码更简洁,也避免了某个分支忘记隐藏动画导致界面卡死的bug。
场景二:资源清理
比如操作 IndexedDB、或者释放 Web Worker 等。
function performComplexCalculation() {
const worker = new Worker('calculator.js');
return new Promise((resolve, reject) => {
worker.onmessage = (e) => resolve(e.data);
worker.onerror = (e) => reject(e.error);
worker.postMessage({ type: 'start', data: [1,2,3,4] });
}).finally(() => {
worker.terminate(); // 计算完成或出错,都终止 worker
console.log("Worker 已终止,资源释放。");
});
}
场景三:记录操作日志
无论操作成功还是失败,都记录一条日志。
function processOrder(orderId) {
return updateOrderInDB(orderId)
.then(() => {
console.log(`订单 ${orderId} 处理成功`);
})
.catch(() => {
console.error(`订单 ${orderId} 处理失败`);
})
.finally(() => {
console.log(`订单 ${orderId} 处理流程结束,记录时间戳。`);
logToAnalytics('order_process_complete', { orderId });
});
}
六、需要注意的细节与区别
1. 回调函数不接收参数
这是 finally() 的一个重要特性。你无法在它的回调函数里知道 Promise 是成功还是失败,这也符合它“只负责清理,不关心结果”的设计哲学。如果你想根据结果做不同的事,应该把逻辑放在 then() 或 catch() 里。
2. 返回值穿透
finally() 返回的新 Promise,最终结果值与原始 Promise 一致,除非 finally() 回调中抛出了异常或返回了一个 rejected 的 Promise。
Promise.resolve(1)
.finally(() => {
console.log('清理中...');
// 没有返回值或抛出异常
})
.then(value => {
console.log(value); // 输出 1,finally 不影响值的传递
});
Promise.reject('错误信息')
.finally(() => {
console.log('清理中...');
})
.catch(reason => {
console.log(reason); // 输出 '错误信息'
});
如果 finally() 回调抛出异常,那么返回的新 Promise 会以这个异常被拒绝。
Promise.resolve(2)
.finally(() => {
throw new Error('finally 里出错了');
})
.catch(err => {
console.log(err.message); // 输出 'finally 里出错了'
});
七、专业名词小结
-
Promise:JavaScript 中用于处理异步操作的对象。
-
fulfilled:已成功,表示 Promise 成功完成。
-
rejected:已失败,表示 Promise 操作失败。
-
settled:已敲定,表示 Promise 最终状态,无论是 fulfilled 还是 rejected。
-
callback:回调函数,作为参数传递给另一个函数的函数。
-
cleanup:清理,指释放资源、重置状态等操作。
八、本节课程知识要点
-
用途:
finally()用于定义无论 Promise 成功或失败都会执行的函数,主要用于清理任务。 -
不接收参数:
finally()的回调函数无法获知 Promise 的结果,这使得它专注于副作用处理。 -
值穿透:
finally()返回的新 Promise 保持原始 Promise 的结果值或拒绝原因。 -
异常处理:如果
finally()回调抛出异常,则返回的新 Promise 会被该异常拒绝。 -
链式调用:
finally()会返回一个 Promise,因此可以在它后面继续调用then()或catch()。 -
与同步
finally类比:行为与同步代码中的try...catch...finally的finally块一致。
掌握 finally() 能让你的 Promise 链更健壮、更简洁。它不是什么复杂的特性,但用好了能让代码质量明显提升。下次写异步代码时,不妨想想:“有什么操作是无论如何都要做的?”如果有,那就交给 finally()。