1. 前置き
ECMAScript仕様を毎日少しずつ読むことが健康によいと判断したのですね。新年の抱負だったのか、 あるいは単に医師からの処方だったのかもしれません。いずれにせよ、ようこそ!
注:この文書では、仕様自体を指す場合にのみ「ECMAScript」という用語を 使用し、それ以外では「JavaScript」を使用します。ただし、どちらの用語も同じ ものを指します。(ECMAScriptとJavaScriptには歴史的な違いがいくつかありますが、それを論じることは この文書の範囲外であり、その違いはGoogleで簡単に検索でき ます。)
1.1. なぜECMAScript仕様を読むべきなのか
ECMAScript仕様は、ブラウザー[WHATISMYBROWSER]、Node.jsを介したサーバー[NODEJS]、またはIoTデバイス[JOHNNY-FIVE]のいずれであっても、あらゆるJavaScript 実装の動作に関する権威ある情報源です。JavaScriptエンジンのすべての開発者は、真新しい機能が意図したとおりに、 ほかのJavaScriptエンジンと同じように動作することを保証するため、この仕様に依存しています。
しかし、この仕様の有用性は、「JavaScriptエンジンの 開発者」として知られる神話上の生き物だけにとどまらないと私は考えています。実際、平均的なJavaScriptプログラマーであるあなたにも役立ちますが、 あなたはまだそれに気づいていないのです。
ある日、仕事中に次のような奇妙な対比を発見したとします
> Array. prototype. push( 42 ) 1 > Array. prototype[ 42 ] > Array. isArray( Array. prototype) true > Set. prototype. add( 42 ) TypeError: Method Set. prototype. add called on incompatible receiver #< Set> at Set. add( < anonymous> ) > Set. prototype Set{}
そして、あるメソッドはそのプロトタイプ上で動作するのに、別のメソッドは そのプロトタイプ上で動作しない理由が分からず、ひどく混乱します。残念ながら、最も必要なときに限ってGoogleは 役に立ちませんし、いつも頼りになるはずの Stack Overflowも同様です。
仕様を読めば役立ちます。
あるいは、悪名高い緩い等価演算子()が実際にどのように機能するのか疑問に思うかもしれません(ここでは「機能する」という語を
緩い意味で使用しています[WAT])。勉強熱心な
ソフトウェアエンジニアらしくMDNで調べてみても、その何段落にも及ぶ説明は、役立つどころか目をさらに
疲れさせるだけです[MDN]。
仕様を読めば役立ちます。
一方、JavaScriptを学び始めたばかりの開発者には、ECMAScript仕様を読むことをお勧めしません。 JavaScriptを始めたばかりなら、Webでいろいろ試してみましょう!Webアプリを作りましょう!あるいは JavaScriptベースの見守りカメラでも、何でも作ってみましょう!そして、JavaScriptの厄介な点を十分に経験するか、 JavaScriptについて心配する必要がないほど裕福になったら、この文書に戻ることを検討してください。
これで、仕様が言語やプラットフォームの複雑な仕組みを理解するうえで非常に役立つ道具になり得ることが分かりました。 しかし、ECMAScript仕様の領域には具体的に何が含まれるのでしょうか?
1.2. ECMAScript仕様に含まれるものと、 含まれないもの
この質問に対する教科書的な答えは、「ECMAScript仕様には言語機能だけが含まれる」です。 しかし、それでは「JavaScriptの機能はJavaScriptである」と言うようなもので、役に立ちません。そして私は トートロジーを好みません[XKCD-703]。
代わりに、JavaScriptアプリでよく見かけるものをいくつか挙げ、それぞれが 言語機能であるかどうかを説明します。
構文要素の構文(つまり、有効な..
ループがどのような形か)
| ✔ |
構文要素の意味論(つまり、またはが何を返すか)
| ✔ |
| ❓[1] |
Object,
Array,
Function,
Number,
Math,
RegExp,
Proxy,
Map,
Promise,
ArrayBuffer,
Uint8Array,
globalThis,
...
| ✔ |
console, setTimeout, setInterval, clearTimeout, clearInterval
| ✘[2] |
Buffer,
process,
global*
| ✘[3] |
module,
exports,
require(),
__dirname,
__filename
| ✘[4] |
window,
alert(),
confirm(),
DOM(document,
HTMLElement,
addEventListener(),
Worker,
...)
| ✘[5] |
[2] これらはブラウザーとNode.jsの両方で利用できますが、標準ではありません。
Node.jsでは、そのドキュメントによって文書化または規定されています。ブラウザーでは、console
はConsole Standard[CONSOLE]によって規定され、残りはHTML
Standard[HTML]によって規定されています。
[3] これらはすべてNode.js専用のグローバルであり、そのドキュメントによって文書化または規定されています。* globalとは異なり、globalThis
はECMAScriptの一部であり、ブラウザーにも実装されていることに注意してください。
[4] これらはNode.js専用のモジュール全体で使用できる「グローバル」であり、そのドキュメントによって文書化または規定されています。
[5] これらはすべてブラウザー専用です。
1.3. これ以上先へ進む前に、ECMAScript仕様はどこに あるのか?
「ECMAScript specification」をGoogleで検索すると、正当な仕様だと主張する結果があまりにも多く表示されます。 どれを読めばよいのでしょうか??
要するに、大抵の場合、tc39.es/ecma262/で公開されている仕様が目的のものです [ECMA-262]。
詳しい説明:
ECMAScript言語仕様は、多様な背景を持つ人々のグループであるEcma International Technical Committee 39(より一般にはTC39[TC39]として知られています)によって開発されています。TC39は ECMAScript言語の最新仕様をtc39.es[ECMA-262]で管理して います。
問題を複雑にしているのは、TC39が毎年ある時点を選び、仕様のスナップショットを その年のECMAScript言語標準として版番号とともに発行することです。たとえば、 ECMAScript® 2019言語仕様(ECMA-262、第10版)[ECMA-262-2019](一般にES10または ES2019として知られています)は、2019年6月時点のtc39.es[ECMA-262]の仕様を、ホルマリン漬けにし、きちんとシュリンク包装し、 永久保存用にPDF化したものにすぎません。
したがって、Webアプリケーションを、ホルマリン漬けにされ、きちんとシュリンク包装され、 永久保存用にPDF化された2019年6月当時のブラウザーだけで動作させたいのでない限り、常に tc39.esの最新仕様[ECMA-262]を参照するべきです。 ただし、古いブラウザーやNode.jsのバージョンをサポートしたい(またはサポートする必要がある)場合は、古い 仕様を参照すると役立つことがあります。
注:ISO/IECもECMAScript言語標準を ISO/IEC 22275[ISO-22275-2018]として再発行しています。ただし、 この標準は基本的に[ECMA-262]へのハイパーリンクなので、気にする必要はありません。
1.4. 仕様内のナビゲーション
ECMAScript仕様は、膨大な量の事柄を扱っています。著者たちは 論理的なまとまりに分割しようと最大限努力していますが、それでも巨大な文書です。
個人的には、仕様を次の5つの部分に分けて考えています:
-
規約と基礎(「Numberとは何か?仕様に『 TypeError例外をスローする』と記載されている場合、それは何を意味するのか?」)
-
言語の文法生成規則(「
-for ループはどのように記述するのか?」)in -
言語の静的意味論(「
文では変数名をどのように決定するのか?」)var -
言語の実行時意味論(「
-for ループはどのように実行されるのか?」)in -
API(「
Stringは 何をするのか?」). prototype. substring()
しかし、仕様はそのようには構成されていません。最初の箇条書きは§5 表記規約から§9 通常オブジェクトおよびエキゾチックオブジェクトの 動作に、次の3つは§10 ECMAScript言語:ソースコードから§15 ECMAScript言語:スクリプトおよび モジュールまでに、次のように入り交じった形で配置されています
§13.6
文 文法生成規則if
§13.6.1-6 静的意味論
§13.6.7 実行時意味論
§13.7 反復文 文法 生成規則
§13.7.1 共通の静的および実行時意味論
§13.7.2
-do 文while
§13.7.2.1-5 静的意味論
§13.7.2.6 実行時意味論
§13.7.3
文while
...
一方、APIは§18 グローバル オブジェクトから§26 リフレクションまでの各節に分散しています。
ここで、仕様を上から下まで読む人は誰一人としていないことを指摘しておきます。 探しているものに対応する節だけを参照し、その節の中でも必要な部分だけを読みます。具体的な質問が 5つの大きな区分のどれに関係するかを判断してください。判断が難しい場合は、 「これ(確認しようとしているもの)はいつ評価されるのか?」と自問すると 役立つことがあります。心配しないでください。仕様内の移動は、練習するほど容易になります。
2. 実行時意味論
言語およびAPIの実行時意味論は仕様の中で最も大きな部分であり、通常、人々が 最も多くの疑問を抱く部分でもあります。
全体として、仕様のこれらの節を読むことはかなり容易です。しかし、仕様では 初めて読む人にとって非常に扱いにくい略記法が多数使用されています(少なくとも私にはそうでした)。ここでは これらの規約のいくつかを説明し、複数のものがどのように 動作するかを調べる一般的な作業手順に適用してみます。
2.1. アルゴリズムのステップ
ECMAScriptの実行時意味論の大部分は、疑似コードに似ていながらも はるかに厳密な形式の、一連のアルゴリズムのステップによって規定されています。
関連資料:§5.2 アルゴリズムの規約
2.2. 抽象操作
仕様では、ときどき関数のようなものが呼び出されているのを目にします。Boolean()
関数の最初のステップは次のとおりです:
Booleanが引数valueで呼び出された場合、次の
ステップを実行する:
-
bを! ToBoolean(value)とする。
-
...
この「ToBoolean」関数は抽象操作と呼ばれます。実際には JavaScriptコードに関数として公開されていないため「抽象」です。これは、仕様の執筆者が 同じことを何度も書かなくて済むよう考案した単なる表記法です。
注:今のところ、ToBooleanの前にある!については気にしないでください。 後ほど§ 2.4 完了レコード、? および!で説明します。
関連資料:§5.2.1 抽象操作
2.3. [[This]]とは何か
ときどき、[[表記]]が 「protoをobj.[[Prototype]]とする。」のように使用されているのを目にします。この表記は厳密には、 現れる文脈に応じていくつかの異なる意味を持ちますが、この表記がJavaScriptコードからは 観測できない何らかの内部プロパティを指すと理解しておけば、多くの場合に対応できます。
正確には3つの異なる意味があり、仕様からの例を使用して説明します。 ただし、今のところは読み飛ばしても構いません。
2.3.1. レコードのフィールド
ECMAScript仕様では、固定されたキーの集合を持つキーと値のマップ(C系言語の 構造体に少し似ています)を指すために、レコードという用語を使用します。レコードの各キーと値の組は、フィールドと呼ばれます。レコードは仕様内にのみ現れ、実際の JavaScriptコードには現れないため、[[表記]]を使用してレコードのフィールドを参照することは理にかなっています。
特に、プロパティ記述子も、 [[Value]]、[[Writable]]、[[Get]]、[[Set]]、[[Enumerable]]、[[Configurable]]というフィールドを持つレコードとして モデル化されています。IsDataDescriptor抽象操作では、この 表記が広範に使用されています:
抽象操作IsDataDescriptorがプロパティ記述子 Descで呼び出された場合、次のステップを実行する:
-
Descがundefinedならば、falseを返す。
-
Desc.[[Value]]とDesc.[[Writable]]の両方が存在しないならば、 falseを返す。
-
trueを返す。
レコードの別の具体例は、 次の節である§ 2.4 完了レコード、? および!にあります。
2.3.2. JavaScriptオブジェクトの内部スロット
JavaScriptオブジェクトには、仕様がデータを保持するために使用する、いわゆる内部スロットが存在する場合があります。 レコードのフィールドと同様に、これらの内部スロットも JavaScriptを使用して観測できませんが、その一部はGoogle ChromeのDevToolsなど、実装固有の ツールを介して公開される場合があります。したがって、[[表記]]を使用して 内部スロットを記述することも理にかなっています。
内部 スロットの詳細については、§ 2.5 JavaScriptオブジェクトで説明します。今のところ、 何に使用されるかはあまり気にせず、次の例だけ確認してください。
ほとんどのJavaScriptオブジェクトには、継承元のオブジェクトを参照する内部スロット[[Prototype]]があります。
この内部スロットの値は通常、Object.getPrototypeOf()
が返す値です。OrdinaryGetPrototypeOf抽象操作では、この
内部スロットの値に
アクセスします:
抽象操作OrdinaryGetPrototypeOfがオブジェクトOで呼び出された場合、次の ステップを実行する:
-
O.[[Prototype]]を返す。
注:オブジェクトの内部スロットとレコードのフィールドは見た目が同一ですが、 この表記の前にあるもの(ドットの前の部分)がオブジェクトであるかレコードであるかを確認すれば区別できます。これは通常、周囲の文脈からかなり明らかです。
2.3.3. JavaScriptオブジェクトの内部 メソッド
JavaScriptオブジェクトには、いわゆる内部メソッドも存在する場合があります。内部スロットと同様に、これらの内部メソッドはJavaScriptから直接 観測できません。したがって、[[表記]]を使用して 内部メソッドを記述することも理にかなっています。
内部 メソッドの詳細については、§ 2.5 JavaScriptオブジェクトで説明します。今のところ、 何に使用されるかはあまり気にせず、次の例だけ確認してください。
すべてのJavaScript関数には、その関数を実行する内部メソッド[[Call]]があります。Call抽象操作には次のステップがあります:
? F.[[Call]](V, argumentsList)を返す。
ここで、FはJavaScript関数オブジェクトです。この場合、Fの[[Call]]内部メソッド自体が 引数VおよびargumentsListで呼び出されます。
注:[[表記]]のこの3番目の意味は、 関数呼び出しのように見えることで、ほかの意味と区別できます。
2.4.
完了レコード、? および!
ECMAScript仕様のすべての実行時意味論は、その結果を報告する完了レコードを 明示的または暗黙的に返します。この完了レコードは、次の3つのフィールドを持ち得るレコードです:
-
[[Type]](
normal、、return 、throw 、またはbreak )continue -
[[Type]]が
normal、return、 またはthrowならば、 [[Value]](「返されるものまたは スローされるもの」)も持つことができます -
[[Type]]が
breakまたはcontinueならば、 この実行時意味論の結果としてスクリプトの実行が中断または継続されるラベルを、 [[Target]]として任意で保持できます
注:レコードのフィールドを表すために2つの角括弧が使用されます。§ 2.3.1 レコードのフィールドで、レコード およびそれに関連する表記法の概要を確認してください。
[[Type]]がnormal
である完了レコードは、
正常
完了と呼ばれます。正常完了以外のすべての完了レコードは、中途
完了とも呼ばれます。
ほとんどの場合、扱うことになるのは[[Type]]がthrowである中途完了だけです。
ほかの3つの中途完了型は、特定の構文要素がどのように評価されるかを確認する場合にのみ役立ちます。
実際、//は
関数の境界を越えて機能しないため、組み込み関数の定義でこれらの型を目にすることはありません。
関連資料:§6.2.3 完了レコード仕様 型
完了レコードの定義により、エラーを-
ブロックまで伝播させるといったJavaScriptの便利な仕組みは、仕様内には存在しません。実際、エラー(より正確には中途完了)は
明示的に処理されます。
略記法を使用しなければ、計算結果を返す場合と エラーをスローする場合がある抽象操作の通常の呼び出しに関する仕様の記述は、次のようになります:
例外をスローする可能性がある抽象操作を略記法なしで 呼び出すいくつかのステップ:
-
resultCompletionRecordをAbstractOp()とする。
注:resultCompletionRecordは完了レコードです。
-
resultCompletionRecordが中途完了ならば、 resultCompletionRecordを返す。
注:ここでは、resultCompletionRecordが 中途完了ならば直接返されます。つまり、AbstractOpでスローされたエラーは 転送され、残りのステップは中止されます。
-
resultをresultCompletionRecord.[[Value]]とする。
-
resultが必要な結果です。これを使ってさらに処理できます。
これは、Cにおける手動のエラー処理を何となく思い起こさせるかもしれません:
int result = abstractOp (); // ステップ1 if ( result < 0 ) // ステップ2 return result ; // ステップ2(続き) // ステップ3は不要 // func()は成功したため、続行する... // ステップ4
しかし、このような定型的なステップを減らすため、ECMAScript仕様の編集者はいくつかの略記法を追加しました。 ES2016以降、同じ仕様の記述を次の2つの同等な方法で書けます:
例外をスローする可能性がある抽象操作をReturnIfAbruptを使用して呼び出すいくつかのステップ:
-
resultをAbstractOp()とする。
注:ここでは、前の例のステップ1と同様に、 resultは完了レコードです。
-
ReturnIfAbrupt(result)。
注:ReturnIfAbruptは、発生し得る 中途完了を転送して処理し、 resultをその[[Value]]へ自動的に展開します。
-
resultが必要な結果です。これを使ってさらに処理できます。
または、特別な疑問符(?)表記を使用して、さらに簡潔に記述できます:
AbstractOpの特定の呼び出しが中途完了を決して返さないと分かっている場合、それを示すことで、 仕様の意図についてより多くの情報を読者に伝えられることがあります。その場合、感嘆 符(!)が使用されます:
!が 有効なJavaScript式のように見える場合、確かに非常に紛らわしくなることがあります:
bを! ToBoolean(value)とする。
—
Boolean()から抜粋。
ここで、!は、 ToBooleanのこの呼び出しが例外を決して 返さないと確信していることを意味するだけであり、結果を反転するという意味ではありません!
関連資料:§5.2.3.4 ReturnIfAbruptの略記法。
2.5. JavaScriptオブジェクト
ECMAScriptでは、すべてのオブジェクトが、仕様のほかの部分から特定の処理を行うために呼び出される 内部メソッドの集合を持ちます。すべてのオブジェクトが持つ 内部メソッドには、次のものがあります:
-
[[Get]]:オブジェクトのプロパティを取得する(例:
obj). prop -
[[Set]]:オブジェクトのプロパティを設定する(例:
obj). prop= 42 ; -
[[GetPrototypeOf]]:オブジェクトのプロトタイプを取得する(つまり、
Object). getPrototypeOf( obj) -
[[GetOwnProperty]]:オブジェクト自身のプロパティのプロパティ記述子を取得する(つまり、
Object). getOwnPropertyDescriptor( obj, "prop" ) -
[[Delete]]:オブジェクトのプロパティを削除する(例:
)delete obj. prop
(完全な一覧は§6.1.7.2 オブジェクトの内部メソッドおよび 内部スロットにあります)。
この定義に基づくと、関数オブジェクト(または単に「関数」)は、[[Call]]内部メソッドと、 場合によっては[[Construct]]内部メソッドも追加で持つオブジェクトにすぎません。 このため、呼び出し可能オブジェクトとも呼ばれます。
仕様では、すべてのオブジェクトを通常オブジェクトとエキゾチックオブジェクトの2つに分類します。目にするオブジェクトの大部分は通常オブジェクトです。これは、そのすべての内部メソッドが、 §9.1 通常オブジェクトの内部 メソッドおよび内部スロットで規定されているデフォルトのものであることを意味します。
ただし、ECMAScript仕様では、これらの内部メソッドのデフォルト実装を上書きできる エキゾチックオブジェクトもいくつか定義されています。 エキゾチックオブジェクトに許される動作には一定の最小限の制約がありますが、一般には、上書きされた内部メソッドは 仕様に反することなく、さまざまな離れ業を実行できます。
Array
オブジェクトは、このようなエキゾチックオブジェクトの一種です。Array
オブジェクトのlengthプロパティに関する特殊な意味論の一部は、
通常オブジェクトが利用できる仕組みでは実現できません。
その1つは、Array
オブジェクトのlengthプロパティを設定すると、オブジェクトからプロパティが削除される可能性がある一方で、
lengthプロパティは単なる通常のデータプロパティに見えることです。対照的に、は
Map上で規定されたgetter関数にすぎず、
が持つ不思議な特性はありません。
> const arr= [ 0 , 1 , 2 , 3 ]; > console. log( arr); [ 0 , 1 , 2 , 3 ] > arr. length= 1 ; > console. log( arr); [ 0 ] > console. log( Object. getOwnPropertyDescriptor([], "length" )); { value: 1 , writable: true , enumerable: false , configurable: false }
> console. log( Object. getOwnPropertyDescriptor( new Map(), "size" )); undefined > console. log( Object. getOwnPropertyDescriptor( Map. prototype, "size" )); { get: [ Function: get size], set: undefined , enumerable: false , configurable: true }
この動作は、[[DefineOwnProperty]]内部メソッドを上書きすることで実現されます。詳細は§9.4.2 Arrayエキゾチックオブジェクトを参照してください。
ECMAScript仕様では、ほかの仕様が独自のエキゾチックオブジェクトを定義することも認めています。ブラウザーが
クロスオリジン
APIアクセスに設ける制限は、この仕組みを通して規定されます(WindowProxyを参照)
[HTML]。JavaScriptプログラマーがProxy
APIを通して独自のエキゾチックオブジェクトを作成することもできます。
JavaScriptオブジェクトには、特定の種類の値を格納するために定義された内部スロットも存在する場合があります。私は
内部
スロットを、Object.getOwnPropertySymbols()からさえ隠された、Symbolを名前とするプロパティのようなものだと考えています。
通常オブジェクトと
エキゾチックオブジェクトはどちらも
内部
スロットを持つことができます。
§ 2.3.2 JavaScriptオブジェクトの
内部スロットでは、ほとんどのオブジェクトが持つ[[Prototype]]という内部スロットについて触れました。(実際、
すべての通常
オブジェクトと、Array
オブジェクトのような一部のエキゾチックオブジェクトさえもこれを持ちます。)しかし、先ほど簡単に
説明した[[GetPrototypeOf]]という内部メソッドも存在します。違いは何でしょうか?
ここで重要なのはほとんどという語です。ほとんどのオブジェクトは[[Prototype]]内部スロットを持ちますが、
すべてのオブジェクトは[[GetPrototypeOf]]内部メソッドを実装します。特に、Proxy
オブジェクトは独自の[[Prototype]]を持たず、その[[GetPrototypeOf]]
内部メソッドは、登録されたハンドラーまたはターゲットのプロトタイプのいずれかに処理を委ねます。ターゲットはProxy
オブジェクトの[[ProxyTarget]]内部スロットに格納されています。
このため、オブジェクトを扱う場合は、内部スロットの値を直接調べるのではなく、適切な内部 メソッドを参照することが、ほぼ常に適切です。
オブジェクト、内部メソッド、および内部スロットの関係は、 古典的なオブジェクト指向の観点から考えることもできます。「オブジェクト」は、実装しなければならない複数の内部メソッドを規定するインターフェイスのようなものです。 通常オブジェクトはデフォルト実装を提供し、エキゾチックオブジェクトはそれを 部分的または完全に上書きできます。一方、内部スロットは、オブジェクトのインスタンス変数、つまり そのオブジェクトの実装の詳細に相当します。
これらの関係はすべて、次のUML図にまとめられています(クリックすると拡大します):
2.6. 例:String. prototype. substring()
仕様がどのように構成され、記述されているかを十分に理解できたので、練習してみましょう!
次のような疑問があるとします:
コードを実行せずに考えた場合、次のコード断片は何を返すでしょうか?
String. prototype. substring. call( undefined , 2 , 4 )
これはかなり難しい問題です。もっともらしい結果が2つあるように見えます:
-
Stringが 最初にundefinedを文字列. prototype. substring() へキャストし、その文字列の位置2と 3の文字(つまり区間[2, 4))を取得して、結果として"undefined" を返す"de" -
一方、
Stringは エラーをスローし、入力としての undefinedを拒否することも十分に考えられます。. prototype. substring()
残念ながら、MDN も、this値が文字列でない場合の関数の動作について、実際のところ何も手掛かりを示していません。
仕様の出番です!仕様[ECMA-262]の
左上にある検索ボックスへsubstringと入力すると、§21.1.3.22 String.prototype.substring(
start、end)に到達します。これは、関数がどのように
動作するかを示す規範的な仕様です。
アルゴリズムのステップを読む前に、まず分かっていることを考えてみましょう。strが通常どのように
動作するか、つまり与えられた文字列の一部を返すことは、基本的に理解しているものとします。現時点でよく分からないのは、
this値がundefinedの場合にどのように動作するかです。そこで、
this値を扱うアルゴリズムのステップを具体的に探します。
幸い、String.prototype.substring()のアルゴリズムの最初のステップは、
this値を直接扱っています:
Oを? RequireObjectCoercible(this 値)とする。
?略記法から、 RequireObjectCoercible抽象操作が実際に 例外をスローする場合があると推測できます。そうでなければ、代わりに!が使用されていたはずだからです。実際、エラーをスローするなら、 先ほどの2番目の仮説に一致します!期待を込めて、ハイパーリンクをクリックし、RequireObjectCoercibleが何をするか 調べます。
RequireObjectCoercible抽象操作は少し変わっています。 ほとんどの抽象 操作とは異なり、ステップではなく表によって定義されています:
引数の型 結果 Undefined TypeError例外をスローする。 ... ...
それでも問題ありません。Undefined(substring()へ渡した
this値の型)に対応する行では、RequireObjectCoercibleが例外をスローするべきだと仕様に記載されています。そして
関数の定義で?表記が
使用されているため、スローされた例外が関数の呼び出し元まで伝播しなければならないことが分かります。これで解決です!
これで答えが分かりました:与えられたコード断片はTypeError例外をスローします。
仕様では、スローされるErrorの型だけを規定し、そこに含まれるメッセージは規定しません。つまり、 実装ごとに異なるエラーメッセージを使用でき、ローカライズされたものにすることもできます。
たとえば、GoogleのV8 6.4(Google Chrome 64に含まれています)では、メッセージは次のとおりです
TypeError: String.prototype.substring called on null or undefined
一方、Mozilla Firefox 57.0では、多少分かりにくい次のメッセージになります
TypeError: can’t convert undefined to object
同時期のChakraCoreバージョン1.7.5.0(Microsoft EdgeのJavaScriptエンジン)はV8と 同じ方針を採り、次のメッセージをスローします
TypeError: String.prototype.substring: 'this' is null or undefined
2.7. 例:Boolean() とString() が例外をスローすることはあるか?
ミッションクリティカルなコードを書く場合、プログラミングでは例外処理を最優先にしなければなりません。そのため、 「ある組み込み関数が例外をスローすることはあるか?」という疑問は、 しばしば考慮されます。
この例では、2つの言語組み込み関数であるBooleanとStringについて、この疑問に答えてみます。ここでは、これらの
関数の直接呼び出しだけを検討し、ボックス化されたオブジェクトを作るとの場合は扱いません。これは間違いなく
JavaScriptで最も
望ましくない機能の1つであり、世の中のほぼすべてのJS
スタイルガイドで強く非推奨とされている慣行です[YDKJS]。
仕様内のBoolean()
の節へ移動すると、アルゴリズムはかなり短いように見えます:
Booleanが引数valueで呼び出された場合、次の
ステップを実行する:
-
bを! ToBoolean(value)とする。
-
NewTargetがundefinedならば、bを返す。
-
Oを? OrdinaryCreateFromConstructor(NewTarget, "%BooleanPrototype%", « [[BooleanData]] »)とする。
-
O.[[BooleanData]]をbに設定する。
-
Oを返す。
しかし一方で、OrdinaryCreateFromConstructorを巡る複雑な処理があり、完全に単純というわけではありません。さらに 重要なのは、ステップ3に? 略記法があり、この関数が特定の場合にエラーをスローする可能性を示していることです。詳しく 見てみましょう。
ステップ1では、value(関数の引数)をBoolean値へキャストします。興味深いことに、このステップには?も!もありませんが、 通常、完了レコードの略記法がないことは!と同じ意味です。したがって、ステップ1が 例外をスローすることはありません。
ステップ2では、NewTargetというものがundefinedかどうかを確認します。NewTargetは、ES2015で初めて追加されたnew.target
メタプロパティに相当する仕様上の概念であり、仕様が呼び出し(この場合はBoolean)とBoolean呼び出し
(この場合はundefined)を区別できるようにします。現時点ではBooleanの直接呼び出しだけを検討しているため、NewTargetは常に
undefinedになり、アルゴリズムは追加の処理を行わず、直ちにbを返すことが分かります。
Booleanをなしで呼び出す場合、Booleanのアルゴリズムでは最初の2つのステップにしか到達せず、
そのどちらも例外をスローしないため、入力にかかわらずBooleanが例外をスローすることはないと結論づけられます。
String()に注目しましょう:
Stringが引数valueで呼び出された場合、次の
ステップを実行する:
-
この関数呼び出しに引数が渡されなかったならば、sを
とする。"" -
そうでなければ、
-
NewTargetがundefinedであり、Type(value)が Symbolならば、SymbolDescriptiveString(value)を返す。
-
sを? ToString(value)とする。
-
-
NewTargetがundefinedならば、sを返す。
-
? StringCreate(s, ? GetPrototypeFromConstructor(NewTarget,
))を返す。"%StringPrototype%"
Boolean()
関数で同じ種類の分析を行った経験から、この場合、NewTargetは常にundefinedになるため、
最後のステップを検討から除外できることが分かります。また、Type
とSymbolDescriptiveStringについても、中途完了が処理されていないため安全だと分かります。それでも、
ToString抽象操作の呼び出しの前には、依然として例の?があります。詳しく見てみましょう。
先ほど確認したRequireObjectCoercibleと同様に、ToString(argument)も表によって定義されています:
引数の型 結果 Undefined を返す。"undefined" Null を返す。"null" Boolean argumentがtrueならば、 を返す。"true" argumentがfalseならば、
を返す。"false" Number NumberToString(argument)を返す。 String argumentを返す。 Symbol TypeError例外をスローする。 Object 次のステップを適用する:
primValueを? ToPrimitive(argument, ヒント String)とする。
? ToString(primValue)を返す。
String()でToStringが呼び出される時点では、
valueはSymbol以外の任意の値になり得ます(Symbolは直前のステップで除外されます)。
それでも、Objectの行には?が2つ残っています。ToPrimitive以降のリンクをたどると、valueがObjectの場合、
実際にエラーをスローする機会が数多くあることが分かります:
String()が
例外をスローするいくつかの例
// 仕様上のスタックトレース: // OrdinaryGet のステップ8。 // 通常オブジェクトの[[Get]]() のステップ1。 // GetV のステップ3。 // GetMethod のステップ2。 // ToPrimitive のステップ2.d。 String({ get[ Symbol. toPrimitive]() { throw new Error( "Breaking JavaScript" ); } });
// 仕様上のスタックトレース: // GetMethod のステップ4。 // ToPrimitive のステップ2.d。 String({ get[ Symbol. toPrimitive]() { return "Breaking JavaScript" ; } });
// 仕様上のスタックトレース: // ToPrimitive のステップ2.e.i。 String({ [ Symbol. toPrimitive]() { throw new Error( "Breaking JavaScript" ); } });
// 仕様上のスタックトレース: // ToPrimitive のステップ2.e.iii。 String({ [ Symbol. toPrimitive]() { return { "breaking" : "JavaScript" }; } });
// 仕様上のスタックトレース: // OrdinaryToPrimitive のステップ5.b.i。 // ToPrimitive のステップ2.g。 String({ toString() { throw new Error( "Breaking JavaScript" ); } });
// 仕様上のスタックトレース: // OrdinaryToPrimitive のステップ5.b.i。 // ToPrimitive のステップ2.g。 String({ valueOf() { throw new Error( "Breaking JavaScript" ); } });
// 仕様上のスタックトレース: // OrdinaryToPrimitive のステップ6。 // ToPrimitive のステップ2.g。 String( Object. create( null ));
したがって、String()については、
プリミティブ値では例外を決してスローしませんが、Objectではエラーをスローする
可能性があると結論づけられます。
2.8.
例:typeof 演算子
ここまではAPI関数だけを分析してきました。別のものを試してみましょう。
執筆予定。<https://github.com/TimothyGu/es-howto/issues/2>
用語集
よく使われる抽象 操作
- ArrayCreate(length[, proto])(仕様)
-
長さがlengthで、protoを [[Prototype]]内部スロットの値とする配列オブジェクトを作成します。protoが指定されていない場合、 現在のレルムの%ArrayPrototype%が使用されます。
Arrayコンストラクターとそのすべてのプロパティがモンキーパッチされておらず、protoが 指定されていないか、現在のレルムの%ArrayPrototype%である場合、と同等です。new Array( length) - Call(F, V[, argumentsList]
)(仕様)
- Construct(F[, argumentsList[, newTarget]])(仕様)
- Get(O, P)(仕様)
- HasProperty(O, P )(仕様)
- Construct(F[, argumentsList[, newTarget]])(仕様)
-
FまたはO上の対応する内部メソッドを、残りの引数を転送して呼び出します。
Reflectオブジェクトの対応するメソッドと同等です。 - DefinePropertyOrThrow(O, P,
desc)(仕様)
- DeletePropertyOrThrow(O, P)(仕様)
-
O上の対応する内部メソッド(それぞれ[[DefineOwnProperty]]および[[Delete]])を、 残りの引数を転送して呼び出します。操作が失敗し、内部メソッドがfalseを返した場合は、例外をスローします。
- GetV(V, P)(仕様)
- HasOwnProperty(O, P)(仕様)
-
O.[[GetOwnProperty]](P)を呼び出して、OがPという名前の 自身のプロパティを持つかどうかを返します。
Objectと同等です。. prototype. hasOwnProperty. call( O, P) - Invoke(V, P[, argumentsList])(仕様)
-
V上のPという名前のメソッドをargumentsListを使用して呼び出します。
Vと同等です。 Callとは異なり、ここでのPはプロパティキーです。[ P](... argumentsList) - IsArray(argument)(仕様)
-
argumentが
Arrayエキゾチックオブジェクトであるか、または argumentがProxyエキゾチックオブジェクトならば、 argumentの最も内側にある[[ProxyTarget]]内部スロットがArrayエキゾチックオブジェクトであるかを返します。Arrayと同等です。. isArray( argument) - IsCallable(argument)(仕様)
-
argumentが呼び出し可能オブジェクト、別名関数 オブジェクトであるかを返します。
と同等です (typeof argument=== 'function' document.allは例外です。 これは複数の特殊な動作を持つエキゾチックオブジェクトです。§B.3.7 [[IsHTMLDDA]]内部スロットを参照してください)。 - IsConstructor(argument)(仕様)
- ReturnIfAbrupt(argument)(仕様)
-
argumentが(スローされた例外のような)中途完了であるかを確認し、そうであれば その中途完了を返します(そして例外が上位へ 伝播できるようにします)。そうでなく、argumentが正常完了ならば、その完了レコードを展開し、 argumentをargument.[[Value]]に設定します。
関連項目:§ 2.4 完了レコード、?および!。
- StringCreate(value, prototype)(仕様)
-
Stringのvalueに対応するボックス化された
Stringオブジェクトを返します。結果のオブジェクトの[[Prototype]]内部スロットは prototypeになります。prototypeが現在のレルムの%StringPrototype%である場合、と同等です。new String( value) - ToBoolean(argument)(仕様)
-
argumentをBooleanへ型強制した値を返します。
と同等です。!! argument - ToInteger(argument)(仕様)
-
ToNumber(argument)を返し、その後、整数になるよう切り捨て (つまり、0の方向へ丸め)ます。
Mathと同等です。. trunc( argument) - ToInt8(argument)(仕様)
- ToUint8(argument)(仕様)
- ToInt16(argument)(仕様)
- ToUint16(argument)(仕様)
- ToInt32(argument)(仕様)
- ToUint32(argument)(仕様)
- ToUint8(argument)(仕様)
-
argumentを切り捨てによって、指定されたビット数と符号性を持つ整数へ変換した値を 返します。
- ToUint8Clamp(argument)(仕様)
-
argumentを丸めとクランプによって範囲[0, 255]の整数へ変換した値を 返します。
- ToNumber(argument)(仕様)
-
argumentをNumberへ型強制した値を返します。
と同等です。+ argument - ToObject(argument)(仕様)
-
必要に応じてボックス化されたプリミティブオブジェクトを使用し、argumentをObjectへ型強制した値を返します。 argumentがundefinedまたは nullの場合を除き、
Objectと同等です。( argument) - ToPrimitive(input[, PreferredType])(仕様)
-
inputをプリミティブ(つまり、非Object)値へ型強制した値を返します。任意で、 PreferredTypeによって指定された型ヒントを使用します。この抽象 操作の正確な意味論は、PreferredTypeによって異なります。
- ToString(argument)(仕様)
-
argumentをStringへ型強制した値を返します。
と同等です。` ${ argument} ` - Type(argument)(仕様)
-
argumentの型を返します。
