JS: try { return } VS finally { return }
JavaScriptでは「try
内のreturn」と「finally
内のreturn」が一連の実行パス上に存在するとき、「finally内のreturn」が優先されるらしい。
「まあそうなるやろな」というのはわかりつつ、仕様としてどうなっているのかが気になってECMAScriptの仕様書を読んだので解説するぞ!
tryの仕様
tryステートメントの仕様はここに書いてある
Syntaxの項を見ると、tryステートメントメントにはざっくりいうと以下のような構文が定義されているようだ (最近catchのパラメータが任意になりましたね)
try { } catch (identifier) { }
try { } finally (identifier) { }
try { } catch (e) finally (identifier) { }
まあこれは皆さんご存知という感じですね。
では実際の動作を定義しているSemanticsの項を読んでみましょう。
この内、try TryStatement : try Block Catch Finally
でfinallyの挙動が定義されています。
雑に翻訳すると
- tryブロックを実行し、その結果をBとする
-
B.[[Type]]
がthrow
なら、catch節にB.[[Value]]
を渡して実行し、その結果をCとする -
B.[[Type]]
がthrow以外ならB(tryの結果)
=C(catchの結果)
とする - finallyを実行し、その結果をFとする
-
F.[[Type]]
がnormalならF(finallyの結果)
=C(Catchの結果)
とする - Completion(UpdateEmpty(F, undefined))を返す
ということらしいっす。
つまり、tryとfinallyの両方にreturnがある場合、
-
4.
でfinally内のreturnが評価され、 -
5.
でF.[[Type]]
がnormalではない(returnになる)ため、F.[[Value]]
が返される
ということだ!
UpdateEmptyとかCompletionとかよくわからない言葉が出てきたので、次はこれにそれらについて調べてみるぞ!
UpdateEmpty(completionRecord, value)
パッといえば、UpdateEmptyは「Completionを生成する」なにかだ。
正確には「completionRecordが正しい状態かをアサーションして、Completionを生成する」役割らしい。
それ以上の定義はないのでこれでUpdateEmptyの解説は終わり!!!!!!!!!!
Completion
ということで、どうやらCompletion
というのがここいらの話の本質っぽい。
詳細なことは理解しきれてないけど、察するに、Completionとは「JavaScriptエンジン内部のフロー制御用の型」で、「ブロックの終了状態を表す型」のようだ(もしかしたら関数の終了状態も表してるかもしれない)
具体的には以下のような構造体っぽい
type Completion = {
[[Type]]: 'normal' | 'break' | 'continue' | 'return' | 'throw'
[[Value]]: any
[[Target]]: string | empty
}
Completion.[[Type]]
がそのフローがどのように終了したかを表している。
「finally内のreturn」 で終了した場合、.[[Type]]
はおそらくreturn
になる。 で、プログラマーが明示的に処理を終了させなかった場合は多分normal
になるんだろうな(知らんけど)
なので、finallyを実行したあとのフローの4.
〜 6.
の仕様で、finally内のreturnが優先される。
4. finallyを実行し、その結果をFとする
- `F.[[Type]]`は return になる
5. `F.[[Type]]`がnormalなら`F(finallyの結果)` = `C(Catchの結果)`とする
- `F.[[Type]]`は normal ではない(return)
- → Fはfinallyの結果のままになる
6. Completion(UpdateEmpty(F, undefined))を返す
- → Finallyの完了状態が返される
そういうことらしいです。
Discussion