如果你写过这样的代码:
function foo() {
try {
return 0;
} finally {
console.log("a");
}
}
console.log(foo());
你可能会惊讶:finally 居然执行了,而且函数依然返回 0!
更离谱的是,如果在 finally 里也写一个 return,会发生什么?
function foo() {
try {
return 0;
} finally {
return 1;
}
}
console.log(foo()); // 1
答案是:finally 的 return 会覆盖 try 的 return!
这就是 JavaScript 语句执行的“黑科技”。
1. 为什么会这样?Completion Record!
JavaScript 引擎在执行语句时,会生成一个 Completion Record:
- [[type]] → 表示语句的类型(normal、break、continue、return、throw);
- [[value]] → 表示语句的返回值(可能是表达式的值);
- [[target]] → 语句目标(主要用于 label 语句)。
当 try 执行到 return 时,会生成一个类型为 return 的 Completion Record。
但是 finally 必须保证执行,所以引擎会在 return 之前先执行 finally。
如果 finally 自己也返回一个 Completion Record,那么它会覆盖之前的结果。
2. 普通语句 & 控制语句的执行逻辑
- 普通语句:顺序执行,结果是
normal,继续往下走。 - 控制语句:if、switch、try/catch,会根据内部产生的 Completion Record 决定是否中断。
- 跳转类语句:break、continue、return、throw,会“穿透”语句块,直到被合适的结构“消费”。
比如:
{
var i = 1; // normal
return i; // return
i++; // 不会执行
}
这个 block 的 Completion Record 类型就是 return,它会直接中断后续执行。
3. finally 的铁律
- finally 一定会执行;
- finally 可以覆盖 try/catch 的 return 或 throw;
- finally 内部的异常,也会覆盖 try/catch 的结果。
这意味着 finally 在 JS 中几乎是“最高优先级”的逻辑。
4. 标签语句:JS 里隐藏的 goto
JS 里其实有个不常用的特性:标签语句。
outer: while (true) {
inner: while (true) {
break outer;
}
}
console.log("finished");
这里的 break outer 会跳出外层循环。
这种机制也是依赖 Completion Record 的 [[target]] 来实现。
总结
- try 里的 return 并不会立即返回,finally 总是会执行。
- finally 的 return/throw 会覆盖 try 的结果。
- JS 用 Completion Record 来统一管理语句执行的结果。
- 标签语句 + break/continue 是 JS 里少见的“控制流黑魔法”。
互动问题:
👉 你在实际项目里,有没有因为 finally 的覆盖机制踩过坑?
欢迎留言分享,大家一起少踩坑!