- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
导读
在 JavaScript 的异常处理中,try...catch之后既可以追加一个finally子句,也可以直接写一段"收尾"代码,二者在无跳转时行为完全一致。本文以 Modern JavaScript Tutorial(本仓库 1-js/10-error-handling/1-try-catch 章节)中的经典辨析题为线索,深入对比这两种写法,揭示finally在函数内部发生return或throw等"跳转退出"时的关键语义差异,并通过可运行的示例代码,帮助读者掌握何时必须使用finally来保证清理逻辑的确定性执行。
题目回顾:两段看似等价的清理代码
原题(1-js/10-error-handling/1-try-catch/1-finally-or-code-after/task.md)给出了两个代码片段,要求判断它们是否完全等价:
片段一:使用finally执行清理
try { // work work } catch (err) { // handle errors } finally { // cleanup the working space }片段二:把清理代码直接放在try...catch之后
try { // work work } catch (err) { // handle errors } // cleanup the working space需求很明确:无论是否发生错误,清理动作都必须执行。仅从这个需求看,两种写法似乎都能满足——如果try块正常结束,两段清理代码都会运行;如果抛出了错误并被catch捕获,两段清理代码也都会在catch结束后运行。
那么,这两段代码真的等价吗?答案是否定的,而差异恰恰隐藏在"函数内部 + 跳转退出"这一组合场景中。官方解答见 1-js/10-error-handling/1-try-catch/1-finally-or-code-after/solution.md。
核心差异:finally能覆盖"跳转退出"
官方解答给出的结论是:当代码位于函数内部,并且存在"跳出try...catch"的行为时,两种写法产生明显差异。这里的"跳出"包含两种常见情形:
try块或catch块中的return语句;catch块中无法处理错误时执行的throw(重新抛出)。
finally子句对try...catch的任何退出路径都生效——包括return和throw,它在try...catch结束之后、控制权交还给调用方之前运行。而"放在函数末尾的普通代码"在这些跳转场景下根本不会执行。
场景一:return跳出try...catch
以下代码来自官方解答:try块中执行了return "result",但finally中的清理仍然会运行:
function f() { try { alert('start'); return "result"; } catch (err) { // ... } finally { alert('cleanup!'); } } f(); // 输出: start, 然后 cleanup!执行流程是:try块执行alert('start'),遇到return时不会立即把控制权交还给调用方,而是先执行finally中的alert('cleanup!'),之后函数才真正返回。如果你把cleanup挪到函数末尾,return会直接跳过它,清理逻辑永远不会执行。
场景二:throw重新抛出错误
再看catch块中重新抛出错误的场景:catch捕获了错误,但判断自己无法处理,于是执行throw err。此时控制流同样会"跳出"try...catch,而finally依然会在错误继续向外传播之前执行:
function f() { try { alert('start'); throw new Error("an error"); } catch (err) { // ... if ("can't handle the error") { throw err; } } finally { alert('cleanup!') } } f(); // 输出: start, 然后 cleanup!,最后错误向外抛出官方解答明确指出:"正是finally在这里保证了清理的执行。如果只是把代码放在函数f的末尾,在这些场景下它根本不会运行。"
finally的完整语义:任何退出路径都执行
语法与执行路径
finally是try...catch的可选第三个子句,完整语法如下(见 article.md 的 "try...catch...finally" 一节):
try { // 尝试执行代码 } catch (err) { // 处理错误 } finally { // 无论何种结果,总是执行 }只要finally存在,它就在所有情况下运行:
try块无错误时,在try之后执行;try块有错误时,在catch之后执行。
下图展示了try...catch的两条基本执行路径(图片来源:try-catch-flow.svg):
交互式验证
主文档中提供了一个可直接在浏览器运行的验证示例:弹窗询问"是否制造一个错误?",回答 "Yes" 时执行顺序为try -> catch -> finally,回答 "No" 时为try -> finally:
try { alert('try'); if (confirm('Make an error?')) BAD_CODE(); } catch (err) { alert('catch'); } finally { alert('finally'); }无论哪条路径,finally都必然出现——这正是它作为"无论如何都要收尾"这一承诺的实现基础。
实战用例:用finally保证耗时测量不遗漏
finally最常见的应用场景是:开始一项需要收尾的工作(如计时、关闭资源),无论成功或失败都必须完成收尾。主文档给出了测量fib(n)执行时间的完整示例:
let num = +prompt("Enter a positive integer number?", 35); let diff, result; function fib(n) { if (n < 0 || Math.trunc(n) != n) { throw new Error("Must not be negative, and also an integer."); } return n <= 1 ? n : fib(n - 1) + fib(n - 2); } let start = Date.now(); try { result = fib(num); } catch (err) { result = 0; } finally { diff = Date.now() - start; } alert(result || "error occurred"); alert(`execution took ${diff}ms`);这里fib对负数或非整数输入会抛出错误。通过把diff = Date.now() - start放进finally,无论fib成功返回还是抛出异常,耗时都能被正确记录:
- 输入
35:正常执行,finally在try之后运行,输出真实耗时; - 输入
-1:立即抛错,catch将result置为 0,finally仍会运行,耗时记录为约0ms。
主文档对此的总结是:函数无论是通过return还是throw结束,finally都会执行。
变量作用域的注意点
上面的例子中,result和diff都声明在try...catch之前,这是有意为之:如果在try块内用let声明,变量只在其内部可见,finally和catch都将无法访问。涉及多个块共享数据时,务必把声明提升到try...catch...finally之外。
finally与return、throw的细节语义
finally在return之前的执行时机
主文档用func()演示了return与finally的先后关系:finally在控制权返回调用方之前执行,因此调用方看到的效果是"先弹出finally的 alert,再得到返回值":
function func() { try { return 1; } catch (err) { /* ... */ } finally { alert('finally'); } } alert(func()); // 先执行 finally 中的 alert,再执行本行不带catch的try...finally
当"不想在这里处理错误(让错误继续向上传播),但必须保证已启动的工作被收尾"时,可以省略catch,只写try...finally:
function func() { // 开始一件需要收尾的事情(如计时) try { // ... } finally { // 即使一切崩溃,也要完成收尾 } }在这种写法中,try块内的错误因为没有catch而必然向外抛出,但finally会在执行流离开函数前先行运行。这与前文throw场景的结论相互印证:finally覆盖的是"任何退出",包括异常路径。
结论与判断准则
回到题目本身:两段代码并不等价。判断依据如下:
| 场景 | finally清理 | 紧随其后的普通清理代码 |
|---|---|---|
try正常结束 | ✅ 执行 | ✅ 执行 |
catch正常结束 | ✅ 执行 | ✅ 执行 |
try/catch中return跳出 | ✅ 执行 | ❌ 被跳过 |
catch中throw重新抛出 | ✅ 执行 | ❌ 被跳过 |
外层嵌套try...catch捕获场景 | ✅ 执行 | ❌ 被跳过 |
实用准则:当清理代码与try...catch处于同一函数,且函数内存在return或重新抛错(throw err)的可能性时,请使用finally而不是"事后代码";当清理代码位于顶层(不在函数内)且不存在跳转路径时,两种写法行为一致,但使用finally依然能提高代码意图的清晰度,让"无论如何都要执行"的语义一目了然。
延伸阅读
- 本章主文档:1-js/10-error-handling/1-try-catch/article.md(含
try...catch基础语法、错误对象、throw、重新抛出与全局捕获window.onerror的完整讲解) - 本题目原文:task.md
- 官方解答:solution.md
- 流程示意图:try-catch-flow.svg
- 下一章(自定义错误类型):2-custom-errors
- 文档/教程
- 前端
【免费下载链接】en.javascript.info
Modern JavaScript Tutorial
相关推荐
IsaacLab 执行器配置避坑:DCMotor 的 4 个关键参数一次讲透
IsaacLab 执行器配置避坑:DCMotor 的 4 个关键参数一次讲透 做 IsaacLab 电机执行器配置时,执行器模型选错,机器人会表现得"不像自己"
人工智能强化学习机器人具身智能深度学习AltStore终极指南:无需越狱,三步安装iOS第三方应用的完整教程
AltStore终极指南:无需越狱,三步安装iOS第三方应用的完整教程 还在为App Store的限制而烦恼吗?想要在iPhone上自由安装各种有趣的应用,但又
移动开发php-src异常处理:try/catch/finally在Zend引擎中的实现
php src异常处理:try/catch/finally在Zend引擎中的实现 引言:异常处理的重要性 在PHP开发中,异常处理(Exception Hand
编程语言语言运行时解释器
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考