ECMAScript仕様の理解、第1部
この記事では、仕様内の簡単な関数を取り上げ、その記法を理解しようとします。さあ、始めましょう!
前書き
JavaScriptを知っていても、その言語仕様であるECMAScript Language specification、略してECMAScript仕様を読むのは非常に気が重い場合があります。少なくとも初めて読んだときはそう感じました。
具体的な例から始めて、仕様を通してそれを理解していきましょう。以下のコードはObject.prototype.hasOwnPropertyの使用例を示しています:
const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false
この例では、oにはhasOwnPropertyというプロパティがないので、プロトタイプチェーンを辿って探します。そして、それをoのプロトタイプであるObject.prototypeの中に見つけます。
Object.prototype.hasOwnPropertyがどのように機能するかを説明するために、仕様は疑似コードのような記述を使用します:
Object.prototype.hasOwnProperty(V)
hasOwnPropertyメソッドが引数Vを使って呼び出される場合、以下の手順が実行されます:
Pを? ToPropertyKey(V)とする。Oを? ToObject(this value)とする。? HasOwnProperty(O, P)を返す。
そして…
抽象操作
HasOwnPropertyは、オブジェクトが指定されたプロパティキーを持つ自身のプロパティを持っているかどうかを判定するために使用されます。ブール値が返されます。この操作は引数O(オブジェクト)とP(プロパティキー)を使って呼び出されます。この抽象操作は以下の手順を実行します:
- 確認:
Type(O)はObjectである。- 確認:
IsPropertyKey(P)はtrueである。descを? O.[[GetOwnProperty]](P)とする。descがundefinedの場合、falseを返す。trueを返す。
しかし、「抽象操作」とは何でしょうか? [[ ]]の中にあるものは何ですか?関数の前の?は何を意味しているのでしょうか?その確認文はどういう意味ですか?
調べていきましょう!
言語型と仕様型
見慣れたものから始めましょう。仕様では、undefined、true、falseなど、JavaScriptから既に知っている値を使用します。これらはすべて言語値、つまり仕様が定義する言語型の値です。
仕様は内部的にも言語値を使用します。例えば、内部データ型がtrueとfalseの値を持つフィールドを含む場合があります。一方、JavaScriptエンジンは通常、内部的に言語値を使用しません。例えば、JavaScriptエンジンがC++で記述されている場合、C++のtrueとfalseを使用することが一般的で、JavaScriptのtrueとfalseを内部表現として使用することはありません。
言語型に加えて、仕様は仕様型も使用します。これは仕様の中だけに現れる型であり、JavaScript言語には存在しません。JavaScriptエンジンはこれを実装する必要はありません(ただし自由に実装することはできます)。このブログ記事では、仕様型レコード(およびそのサブタイプのCompletion Record)について知ることになります。
抽象操作
抽象操作は、ECMAScript仕様で定義される関数で、仕様を簡潔に記述する目的で定義されています。JavaScriptエンジンはそれらをエンジン内で別々の関数として実装する必要はありません。それらはJavaScriptから直接呼び出すことはできません。
内部スロットと内部メソッド
内部スロットおよび内部メソッドでは、名前を[[ ]]で囲んでいます。
内部スロットは、JavaScriptオブジェクトや仕様型のデータメンバーで、オブジェクトの状態を保存するために使用されます。内部メソッドは、JavaScriptオブジェクトのメンバー関数です。
例えば、すべてのJavaScriptオブジェクトには、内部スロット[[Prototype]]と内部メソッド[[GetOwnProperty]]があります。
内部スロットとメソッドはJavaScriptからアクセスできません。例えば、o.[[Prototype]]にアクセスしたり、o.[[GetOwnProperty]]()を呼び出すことはできません。JavaScriptエンジンはこれらを内部的に使用するために実装することができますが、必ずしもそうする必要はありません。
内部メソッドは、通常、同名の抽象操作に委任する場合があります。例えば、通常オブジェクトの[[GetOwnProperty]]に関しては:
Oの[[GetOwnProperty]]内部メソッドがプロパティキーPと共に呼び出されるとき、以下のステップを実行する:
! OrdinaryGetOwnProperty(O, P)を返す。
(感嘆符が何を意味するのかについては次の章で学びます。)
OrdinaryGetOwnProperty は内部メソッドではありません。オブジェクトと関連付けられているわけではないので、代わりに操作対象のオブジェクトがパラメータとして渡されます。
OrdinaryGetOwnProperty は「通常」と呼ばれるのは、通常のオブジェクトに対して操作するためです。ECMAScript のオブジェクトは 通常 (ordinary) か 特殊 (exotic) のいずれかです。通常のオブジェクトは、基本的な内部メソッド (essential internal methods) と呼ばれる一連のメソッドのデフォルトの挙動を持たなければなりません。オブジェクトがデフォルトの挙動から逸脱する場合、それは特殊なオブジェクトとなります。
最もよく知られている特殊なオブジェクトは Array です。length プロパティが非デフォルトな方法で動作するためです: length プロパティを設定すると、Array から要素が削除される可能性があります。
基本的な内部メソッドは こちら にリストされています。
完了記録 (Completion records)
では、疑問符や感嘆符についてはどうでしょうか?それらを理解するためには、完了記録 (Completion Records) を調べる必要があります!
完了記録は仕様タイプであり(仕様目的でのみ定義されています)、JavaScript エンジンにはそれに対応する内部データ型は不要です。
完了記録は「記録」であり、固定された名前付きフィールドのセットを持つデータ型です。完了記録には以下の3つのフィールドがあります:
| 名前 | 説明 |
|---|---|
[[Type]] | 以下のいずれか: normal, break, continue, return, throw。normal を除くすべての型は 急停止完了 (abrupt completions) を意味します。 |
[[Value]] | 完了が発生した際に生成された値。例えば、関数の戻り値や例外(スローされた場合)。 |
[[Target]] | 指定された制御転送に使用される(このブログ記事では関連しません)。 |
すべての抽象操作は暗黙的に完了記録を返します。それが単純な型(例えば Boolean)のように見えても、それは暗黙的に normal 型の完了記録にラップされます(暗黙の完了値 を参照)。
注意点 1: 仕様はこの点で完全に一貫しているわけではありません。一部の補助関数は裸の値を返し、その返り値がそのまま使用され、完了記録から値を抽出することはありません。通常、文脈から明確になります。
注意点 2: 仕様編集者たちは、完了記録の扱いをより明示的にする方向を検討しています。
アルゴリズムが例外をスローする場合、それは [[Type]] が throw の完了記録を返し、その [[Value]] が例外オブジェクトとなることを意味します。今回は break, continue, return タイプについては無視します。
ReturnIfAbrupt(argument) は以下のステップを意味します:
argumentが急停止の場合、argumentを返す。argumentをargument.[[Value]]に設定する。
つまり、完了記録を調べ、急停止完了の場合は即座に返ります。それ以外の場合は、完了記録から値を抽出します。
ReturnIfAbrupt は関数呼び出しのように見えるかもしれませんが、そうではありません。ReturnIfAbrupt() が出現する関数自体を返すことを意味し、ReturnIfAbrupt 関数そのものではありません。それは C 言語のような言語のマクロのように動作します。
ReturnIfAbrupt は次のように使用できます:
objをFoo()とする。(objは完了記録である。)ReturnIfAbrupt(obj)。Bar(obj)。(ここまでたどり着ける場合、objは完了記録から抽出された値である。)
そして、疑問符 が登場: ? Foo() は ReturnIfAbrupt(Foo()) と同等です。略記法を使用することで、エラー処理コードを毎回明示的に書く必要性がなくなります。
同様に、Let val be ! Foo() は以下と同等です:
valをFoo()とする。valが急停止完了でないことをアサートする。valをval.[[Value]]に設定する。
この知識を使って、Object.prototype.hasOwnProperty を次のように書き換えることができます:
Object.prototype.hasOwnProperty(V)
PをToPropertyKey(V)とする。Pが突然の完了である場合、Pを返す。PをP.[[Value]]にセットする。OをToObject(this value)とする。Oが突然の完了である場合、Oを返す。OをO.[[Value]]にセットする。tempをHasOwnProperty(O, P)とする。tempが突然の完了である場合、tempを返す。tempをtemp.[[Value]]にセットする。NormalCompletion(temp)を返す。
…そして HasOwnProperty を次のように書き直すことができます:
HasOwnProperty(O, P)
- 確認:
Type(O)はObjectである。- 確認:
IsPropertyKey(P)がtrueである。descをO.[[GetOwnProperty]](P)とする。descが突然の完了である場合、descを返す。descをdesc.[[Value]]にセットする。descがundefinedの場合、NormalCompletion(false)を返す。NormalCompletion(true)を返す。
[[GetOwnProperty]] 内部メソッドを感嘆符なしで書き直すこともできます:
O.[[GetOwnProperty]]
tempをOrdinaryGetOwnProperty(O, P)とする。- 確認:
tempは突然の完了ではない。tempをtemp.[[Value]]にセットする。NormalCompletion(temp)を返す。
ここでは、temp が他と衝突しない全く新しい一時変数であると仮定しています。
また、return 文が完了記録以外のものを返す場合、それが暗黙的に NormalCompletion にラップされるという知識も利用しています。
付随話題: Return ? Foo()
仕様では Return ? Foo() という表記が使用されています — なぜ疑問符?
Return ? Foo() は次のように展開されます:
tempをFoo()とする。tempが突然の完了である場合、tempを返す。tempをtemp.[[Value]]にセットする。NormalCompletion(temp)を返す。
これは Return Foo() と同じであり、突然の完了と通常の完了の両方に対して同じように振る舞います。
Return ? Foo() は編集の理由だけで使用されており、Foo が完了記録を返すことをより明確に示すためのものです。
アサーション
仕様内のアサーションはアルゴリズムの不変条件を主張します。これらは明確さのために追加されていますが、実装に要件を追加するものではありません — 実装はそれらを確認する必要はありません。
続き
抽象操作は他の抽象操作に委任します(以下の図を参照してください)が、このブログ記事に基づいてそれらが何をするかを理解できるはずです。プロパティ記述子にも遭遇しますが、それは別の仕様型です。
要約
単純なメソッド — Object.prototype.hasOwnProperty — およびそれが呼び出す 抽象操作 を詳しく調べました。エラーハンドリングに関連する短縮記号 ? および ! を確認しました。言語型、仕様型、内部スロット、および 内部メソッド にも遭遇しました。
役に立つリンク
ECMAScript仕様の読み方: 本投稿で取り上げた内容の多くを、若干異なる角度から網羅しているチュートリアル。