ECMAScript仕様の読み方

現行文書、

この版:
https://timothygu.me/es-howto/
課題追跡:
GitHub
仕様内のインライン表示
著者:
Timothy Gu

概要

ECMAScript言語仕様(別名JavaScript仕様またはECMA-262)は、JavaScriptがどのように動作するか、その複雑な仕組みを 学ぶための優れた資料です。しかし、これは非常に膨大な文書であり、最初は分かりにくく、 気後れするかもしれません。この文書は、利用可能な最良のJavaScript 言語リファレンスを読み始めやすくすることを目的としています。

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アプリでよく見かけるものをいくつか挙げ、それぞれが 言語機能であるかどうかを説明します。

構文要素の構文(つまり、有効なfor..in ループがどのような形か)
構文要素の意味論(つまり、typeof nullまたは{ a: b }が何を返すか)
import a from 'a'; [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]
[1] ECMAScript仕様は、このような宣言の構文と、それらが何を意味するべきかを規定 しますが、モジュールがどのように読み込まれるかは規定しません。

[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]へのハイパーリンクなので、気にする必要はありません。

ECMAScript仕様は、膨大な量の事柄を扱っています。著者たちは 論理的なまとまりに分割しようと最大限努力していますが、それでも巨大な文書です。

個人的には、仕様を次の5つの部分に分けて考えています:

しかし、仕様はそのようには構成されていません。最初の箇条書きは§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の実行時意味論の大部分は、疑似コードに似ていながらも はるかに厳密な形式の、一連のアルゴリズムのステップによって規定されています。

アルゴリズムのステップの例を次に示します:

  1. a1とする。

  2. ba+aとする。

  3. b2ならば、

    1. やった!算術は壊れていません。

  4. そうでなければ、

    1. 残念!

関連資料:§5.2 アルゴリズムの規約

2.2. 抽象操作

仕様では、ときどき関数のようなものが呼び出されているのを目にします。Boolean() 関数の最初のステップは次のとおりです:

Booleanが引数valueで呼び出された場合、次の ステップを実行する:

  1. bを! ToBoolean(value)とする。

  2. ...

この「ToBoolean」関数は抽象操作と呼ばれます。実際には JavaScriptコードに関数として公開されていないため「抽象」です。これは、仕様の執筆者が 同じことを何度も書かなくて済むよう考案した単なる表記法です。

注:今のところ、ToBooleanの前にある!については気にしないでください。 後ほど§ 2.4 完了レコード、? および!で説明します。

関連資料:§5.2.1 抽象操作

2.3. [[This]]とは何か

ときどき、[[表記]]が 「protoobj.[[Prototype]]とする。」のように使用されているのを目にします。この表記は厳密には、 現れる文脈に応じていくつかの異なる意味を持ちますが、この表記がJavaScriptコードからは 観測できない何らかの内部プロパティを指すと理解しておけば、多くの場合に対応できます。

正確には3つの異なる意味があり、仕様からの例を使用して説明します。 ただし、今のところは読み飛ばしても構いません。

2.3.1. レコードのフィールド

ECMAScript仕様では、固定されたキーの集合を持つキーと値のマップ(C系言語の 構造体に少し似ています)を指すために、レコードという用語を使用します。レコードの各キーと値の組は、フィールドと呼ばれます。レコードは仕様内にのみ現れ、実際の JavaScriptコードには現れないため、[[表記]]を使用してレコードフィールドを参照することは理にかなっています。

特に、プロパティ記述子も、 [[Value]]、[[Writable]]、[[Get]]、[[Set]]、[[Enumerable]]、[[Configurable]]というフィールドを持つレコードとして モデル化されています。IsDataDescriptor抽象操作では、この 表記が広範に使用されています:

抽象操作IsDataDescriptorがプロパティ記述子 Descで呼び出された場合、次のステップを実行する:

  1. Descundefinedならば、falseを返す。

  2. Desc.[[Value]]とDesc.[[Writable]]の両方が存在しないならば、 falseを返す。

  3. trueを返す。

レコードの別の具体例は、 次の節である§ 2.4 完了レコード、? および!にあります。

関連資料:§6.2.1 リストおよびレコード仕様 型

2.3.2. JavaScriptオブジェクトの内部スロット

JavaScriptオブジェクトには、仕様がデータを保持するために使用する、いわゆる内部スロットが存在する場合があります。 レコードのフィールドと同様に、これらの内部スロットも JavaScriptを使用して観測できませんが、その一部はGoogle ChromeのDevToolsなど、実装固有の ツールを介して公開される場合があります。したがって、[[表記]]を使用して 内部スロットを記述することも理にかなっています。

内部 スロットの詳細については、§ 2.5 JavaScriptオブジェクトで説明します。今のところ、 何に使用されるかはあまり気にせず、次の例だけ確認してください。

ほとんどのJavaScriptオブジェクトには、継承元のオブジェクトを参照する内部スロット[[Prototype]]があります。 この内部スロットの値は通常、Object.getPrototypeOf() が返す値です。OrdinaryGetPrototypeOf抽象操作では、この 内部スロットの値に アクセスします:

抽象操作OrdinaryGetPrototypeOfがオブジェクトOで呼び出された場合、次の ステップを実行する:

  1. O.[[Prototype]]を返す。

注:オブジェクトの内部スロットレコードのフィールドは見た目が同一ですが、 この表記の前にあるもの(ドットの前の部分)がオブジェクトであるかレコードであるかを確認すれば区別できます。これは通常、周囲の文脈からかなり明らかです。

2.3.3. JavaScriptオブジェクトの内部 メソッド

JavaScriptオブジェクトには、いわゆる内部メソッドも存在する場合があります。内部スロットと同様に、これらの内部メソッドはJavaScriptから直接 観測できません。したがって、[[表記]]を使用して 内部メソッドを記述することも理にかなっています。

内部 メソッドの詳細については、§ 2.5 JavaScriptオブジェクトで説明します。今のところ、 何に使用されるかはあまり気にせず、次の例だけ確認してください。

すべてのJavaScript関数には、その関数を実行する内部メソッド[[Call]]があります。Call抽象操作には次のステップがあります:

  1. ? F.[[Call]](V, argumentsList)を返す。

ここで、FはJavaScript関数オブジェクトです。この場合、Fの[[Call]]内部メソッド自体が 引数VおよびargumentsListで呼び出されます。

注:[[表記]]のこの3番目の意味は、 関数呼び出しのように見えることで、ほかの意味と区別できます。

2.4. 完了レコード、?および!

ECMAScript仕様のすべての実行時意味論は、その結果を報告する完了レコードを 明示的または暗黙的に返します。この完了レコードは、次の3つのフィールドを持ち得るレコードです:

注:レコードフィールドを表すために2つの角括弧が使用されます。§ 2.3.1 レコードのフィールドで、レコード およびそれに関連する表記法の概要を確認してください。

[[Type]]normal である完了レコードは、 正常 完了と呼ばれます。正常完了以外のすべての完了レコードは、中途 完了とも呼ばれます。

ほとんどの場合、扱うことになるのは[[Type]]throwである中途完了だけです。 ほかの3つの中途完了型は、特定の構文要素がどのように評価されるかを確認する場合にのみ役立ちます。 実際、break/continue/returnは 関数の境界を越えて機能しないため、組み込み関数の定義でこれらの型を目にすることはありません。

関連資料:§6.2.3 完了レコード仕様 型


完了レコードの定義により、エラーをtry-catch ブロックまで伝播させるといったJavaScriptの便利な仕組みは、仕様内には存在しません。実際、エラー(より正確には中途完了)は 明示的に処理されます。

略記法を使用しなければ、計算結果を返す場合と エラーをスローする場合がある抽象操作の通常の呼び出しに関する仕様の記述は、次のようになります:

例外をスローする可能性がある抽象操作を略記法なしで 呼び出すいくつかのステップ:

  1. resultCompletionRecordをAbstractOp()とする。

    注:resultCompletionRecord完了レコードです。

  2. resultCompletionRecordが中途完了ならば、 resultCompletionRecordを返す。

    注:ここでは、resultCompletionRecord中途完了ならば直接返されます。つまり、AbstractOpでスローされたエラーは 転送され、残りのステップは中止されます。

  3. resultresultCompletionRecord.[[Value]]とする。

    注:正常完了を取得したことを確認した後、完了レコードを展開し、必要な計算の実際の結果を 取得できます。

  4. resultが必要な結果です。これを使ってさらに処理できます。

これは、Cにおける手動のエラー処理を何となく思い起こさせるかもしれません:

int result = abstractOp();              // ステップ1
if (result < 0)                         // ステップ2
  return result;                        // ステップ2(続き)
                                        // ステップ3は不要
// func()は成功したため、続行する...     // ステップ4

しかし、このような定型的なステップを減らすため、ECMAScript仕様の編集者はいくつかの略記法を追加しました。 ES2016以降、同じ仕様の記述を次の2つの同等な方法で書けます:

例外をスローする可能性がある抽象操作をReturnIfAbruptを使用して呼び出すいくつかのステップ:

  1. resultをAbstractOp()とする。

    注:ここでは、前の例のステップ1と同様に、 result完了レコードです。

  2. ReturnIfAbrupt(result)。

    注:ReturnIfAbruptは、発生し得る 中途完了を転送して処理し、 resultをその[[Value]]へ自動的に展開します。

  3. resultが必要な結果です。これを使ってさらに処理できます。

または、特別な疑問符(?)表記を使用して、さらに簡潔に記述できます:

例外をスローする可能性がある抽象操作を疑問符 (?)を使用して呼び出すいくつかのステップ:

  1. resultを? AbstractOp()とする。

    注:この表記では、完了レコードを一切扱いません。? 略記法がすべてを処理するため、その直後からresultを使用できます。

  2. resultが必要な結果です。これを使ってさらに処理できます。


AbstractOpの特定の呼び出しが中途完了を決して返さないと分かっている場合、それを示すことで、 仕様の意図についてより多くの情報を読者に伝えられることがあります。その場合、感嘆 符(!)が使用されます:

決して例外をスローしない抽象操作を感嘆符(!)を使用して呼び出すいくつかのステップ:

  1. resultを! AbstractOp()とする。

    注:?は発生した可能性のあるエラーを 転送しますが、!は、この呼び出しから中途完了を決して受け取らないと 表明し、もし受け取ったなら仕様のバグであることを示します。?の場合と同様に、完了レコードは一切 扱いません。その直後からresultを使用できます。

  2. resultが必要な結果です。これを使ってさらに処理できます。

注意

!が 有効なJavaScript式のように見える場合、確かに非常に紛らわしくなることがあります:

  1. bを! ToBoolean(value)とする。

Boolean()から抜粋。

ここで、!は、 ToBooleanのこの呼び出しが例外を決して 返さないと確信していることを意味するだけであり、結果を反転するという意味ではありません

関連資料:§5.2.3.4 ReturnIfAbruptの略記法

2.5. JavaScriptオブジェクト

ECMAScriptでは、すべてのオブジェクトが、仕様のほかの部分から特定の処理を行うために呼び出される 内部メソッドの集合を持ちます。すべてのオブジェクトが持つ 内部メソッドには、次のものがあります:

(完全な一覧は§6.1.7.2 オブジェクトの内部メソッドおよび 内部スロットにあります)。

この定義に基づくと、関数オブジェクト(または単に「関数」)は、[[Call]]内部メソッドと、 場合によっては[[Construct]]内部メソッドも追加で持つオブジェクトにすぎません。 このため、呼び出し可能オブジェクトとも呼ばれます。

仕様では、すべてのオブジェクトを通常オブジェクトエキゾチックオブジェクトの2つに分類します。目にするオブジェクトの大部分は通常オブジェクトです。これは、そのすべての内部メソッドが、 §9.1 通常オブジェクトの内部 メソッドおよび内部スロットで規定されているデフォルトのものであることを意味します。

ただし、ECMAScript仕様では、これらの内部メソッドのデフォルト実装を上書きできる エキゾチックオブジェクトもいくつか定義されています。 エキゾチックオブジェクトに許される動作には一定の最小限の制約がありますが、一般には、上書きされた内部メソッドは 仕様に反することなく、さまざまな離れ業を実行できます。

Array オブジェクトは、このようなエキゾチックオブジェクトの一種です。Array オブジェクトのlengthプロパティに関する特殊な意味論の一部は、 通常オブジェクトが利用できる仕組みでは実現できません。

その1つは、Array オブジェクトのlengthプロパティを設定すると、オブジェクトからプロパティが削除される可能性がある一方で、 lengthプロパティは単なる通常のデータプロパティに見えることです。対照的に、new Map().sizeMap.prototype上で規定されたgetter関数にすぎず、 [].lengthが持つ不思議な特性はありません。

> 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つあるように見えます:

  1. String.prototype.substring()が 最初にundefinedを文字列"undefined"へキャストし、その文字列の位置2と 3の文字(つまり区間[2, 4))を取得して、結果として"de"を返す

  2. 一方、String.prototype.substring()エラーをスローし、入力としての undefinedを拒否することも十分に考えられます。

残念ながら、MDN も、this値が文字列でない場合の関数の動作について、実際のところ何も手掛かりを示していません。

仕様の出番です!仕様[ECMA-262]の 左上にある検索ボックスへsubstringと入力すると、§21.1.3.22 String.prototype.substring( startendに到達します。これは、関数がどのように 動作するかを示す規範的な仕様です。

アルゴリズムのステップを読む前に、まず分かっていることを考えてみましょう。str.substring()が通常どのように 動作するか、つまり与えられた文字列の一部を返すことは、基本的に理解しているものとします。現時点でよく分からないのは、 this値がundefinedの場合にどのように動作するかです。そこで、 this値を扱うアルゴリズムのステップを具体的に探します。

幸い、String.prototype.substring()のアルゴリズムの最初のステップは、 this値を直接扱っています:

  1. Oを? RequireObjectCoerciblethis 値)とする。

?略記法から、 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()について、この疑問に答えてみます。ここでは、これらの 関数の直接呼び出しだけを検討し、ボックス化されたオブジェクトを作るnew Boolean()new String()の場合は扱いません。これは間違いなく JavaScriptで最も 望ましくない機能の1つであり、世の中のほぼすべてのJS スタイルガイドで強く非推奨とされている慣行です[YDKJS]

仕様内のBoolean() の節へ移動すると、アルゴリズムはかなり短いように見えます:

Booleanが引数valueで呼び出された場合、次の ステップを実行する:

  1. bを! ToBoolean(value)とする。

  2. NewTargetがundefinedならば、bを返す。

  3. Oを? OrdinaryCreateFromConstructor(NewTarget, "%BooleanPrototype%", « [[BooleanData]] »)とする。

  4. O.[[BooleanData]]をbに設定する。

  5. Oを返す。

しかし一方で、OrdinaryCreateFromConstructorを巡る複雑な処理があり、完全に単純というわけではありません。さらに 重要なのは、ステップ3に? 略記法があり、この関数が特定の場合にエラーをスローする可能性を示していることです。詳しく 見てみましょう。

ステップ1では、value(関数の引数)をBoolean値へキャストします。興味深いことに、このステップには?!もありませんが、 通常、完了レコードの略記法がないことは!と同じ意味です。したがって、ステップ1が 例外をスローすることはありません。

ステップ2では、NewTargetというものがundefinedかどうかを確認します。NewTargetは、ES2015で初めて追加されたnew.target メタプロパティに相当する仕様上の概念であり、仕様がnew Boolean()呼び出し(この場合はBoolean)とBoolean()呼び出し (この場合はundefined)を区別できるようにします。現時点ではBoolean()の直接呼び出しだけを検討しているため、NewTargetは常に undefinedになり、アルゴリズムは追加の処理を行わず、直ちにbを返すことが分かります。

Boolean()newなしで呼び出す場合、Boolean()のアルゴリズムでは最初の2つのステップにしか到達せず、 そのどちらも例外をスローしないため、入力にかかわらずBoolean()が例外をスローすることはないと結論づけられます。


String()に注目しましょう:

Stringが引数valueで呼び出された場合、次の ステップを実行する:

  1. この関数呼び出しに引数が渡されなかったならば、s""とする。

  2. そうでなければ、

    1. NewTargetがundefinedであり、Type(value)が Symbolならば、SymbolDescriptiveString(value)を返す。

    2. sを? ToString(value)とする。

  3. NewTargetがundefinedならば、sを返す。

  4. ? StringCreate(s, ? GetPrototypeFromConstructor(NewTarget, "%StringPrototype%"))を返す。

Boolean() 関数で同じ種類の分析を行った経験から、この場合、NewTargetは常にundefinedになるため、 最後のステップを検討から除外できることが分かります。また、TypeSymbolDescriptiveStringについても、中途完了が処理されていないため安全だと分かります。それでも、 ToString抽象操作の呼び出しの前には、依然として例の?があります。詳しく見てみましょう。

先ほど確認したRequireObjectCoercibleと同様に、ToString(argument)も表によって定義されています:

引数の型 結果
Undefined "undefined"を返す。
Null "null"を返す。
Boolean argumenttrueならば、"true"を返す。

argumentfalseならば、"false"を返す。

Number NumberToString(argument)を返す。
String argumentを返す。
Symbol TypeError例外をスローする。
Object

次のステップを適用する:

  1. primValueを? ToPrimitive(argument, ヒント String)とする。

  2. ? 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>

用語集

よく使われる抽象 操作

ArrayCreatelength[, proto])(仕様

長さがlengthで、protoを [[Prototype]]内部スロットの値とする配列オブジェクトを作成します。protoが指定されていない場合、 現在のレルム%ArrayPrototype%が使用されます。Array コンストラクターとそのすべてのプロパティがモンキーパッチされておらず、protoが 指定されていないか、現在のレルム%ArrayPrototype%である場合、new Array(length)と同等です。

CallF, V[, argumentsList] )(仕様
ConstructF[, argumentsList[, newTarget]])(仕様
GetO, P)(仕様
HasPropertyO, P )(仕様

FまたはO上の対応する内部メソッドを、残りの引数を転送して呼び出します。Reflect オブジェクトの対応するメソッドと同等です。

DefinePropertyOrThrowO, P, desc)(仕様
DeletePropertyOrThrowO, P)(仕様

O上の対応する内部メソッド(それぞれ[[DefineOwnProperty]]および[[Delete]])を、 残りの引数を転送して呼び出します。操作が失敗し、内部メソッドfalseを返した場合は、例外をスローします。

GetVV, P)(仕様

必要なら最初にToObjectによってVをObjectへ変換し、 Get(V, P)を返します。V[P]と同等です。

HasOwnPropertyO, P)(仕様

O.[[GetOwnProperty]](P)を呼び出して、OPという名前の 自身のプロパティを持つかどうかを返します。Object.prototype.hasOwnProperty.call(O, P)と同等です。

InvokeV, P[, argumentsList])(仕様

V上のPという名前のメソッドをargumentsListを使用して呼び出します。V[P](...argumentsList)と同等です。 Callとは異なり、ここでのPはプロパティキーです。

IsArrayargument)(仕様

argumentArray エキゾチックオブジェクトであるか、または argumentProxy エキゾチックオブジェクトならば、 argumentの最も内側にある[[ProxyTarget]]内部スロットArray エキゾチックオブジェクトであるかを返します。 Array.isArray(argument)と同等です。

IsCallableargument)(仕様

argument呼び出し可能オブジェクト、別名関数 オブジェクトであるかを返します。typeof argument === 'function'と同等です (document.allは例外です。 これは複数の特殊な動作を持つエキゾチックオブジェクトです。§B.3.7 [[IsHTMLDDA]]内部スロットを参照してください)。

IsConstructorargument)(仕様

argumentが[[Construct]]内部 メソッドを持つ関数オブジェクトであるかを返します。

ReturnIfAbruptargument)(仕様

argumentが(スローされた例外のような)中途完了であるかを確認し、そうであれば その中途完了を返します(そして例外が上位へ 伝播できるようにします)。そうでなく、argument正常完了ならば、その完了レコードを展開し、 argumentargument.[[Value]]に設定します。

関連項目:§ 2.4 完了レコード、?および!

StringCreatevalue, prototype)(仕様

Stringのvalueに対応するボックス化されたString オブジェクトを返します。結果のオブジェクトの[[Prototype]]内部スロットは prototypeになります。prototype現在のレルム%StringPrototype%である場合、new String(value)と同等です。

ToBooleanargument)(仕様

argumentをBooleanへ型強制した値を返します。!!argumentと同等です。

ToIntegerargument)(仕様

ToNumber(argument)を返し、その後、整数になるよう切り捨て (つまり、0の方向へ丸め)ます。Math.trunc(argument)と同等です。

ToInt8argument)(仕様
ToUint8argument)(仕様
ToInt16argument)(仕様
ToUint16argument)(仕様
ToInt32argument)(仕様
ToUint32argument)(仕様

argumentを切り捨てによって、指定されたビット数と符号性を持つ整数へ変換した値を 返します。

ToUint8Clampargument)(仕様

argumentを丸めとクランプによって範囲[0, 255]の整数へ変換した値を 返します。

ToNumberargument)(仕様

argumentをNumberへ型強制した値を返します。+argumentと同等です。

ToObjectargument)(仕様

必要に応じてボックス化されたプリミティブオブジェクトを使用し、argumentをObjectへ型強制した値を返します。 argumentundefinedまたは nullの場合を除き、Object(argument)と同等です。

ToPrimitiveinput[, PreferredType])(仕様

inputをプリミティブ(つまり、非Object)値へ型強制した値を返します。任意で、 PreferredTypeによって指定された型ヒントを使用します。この抽象 操作の正確な意味論は、PreferredTypeによって異なります。

ToStringargument)(仕様

argumentをStringへ型強制した値を返します。`${argument}`と同等です。

String(argument)argument + ''も、ToStringと完全には同等でないことに注意する必要があります。String() はSymbol値をそのString記述へ変換しますが、ToStringはSymbolに対して 例外をスローします。加算演算子は、値をStringへ変換しようとするとき、argument[Symbol.toPrimitive]などの別の関数を呼び出します。
Typeargument)(仕様

argumentを返します。

Index

Terms defined by this specification

Terms defined by reference

References

Informative References

[CONSOLE]
Dominic Farolino; Terin Stock; Robert Kowalski. Console Standard. Living Standard. URL: https://console.spec.whatwg.org/
[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[ECMA-262]
ECMAScript Language Specification. URL: https://tc39.es/ecma262/
[ECMA-262-2019]
ECMAScript 2019 Language Specification. URL: https://ecma-international.org/ecma-262/10.0/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[ISO-22275-2018]
ISO/IEC 22275:2018 - Information technology — Programming languages, their environments, and system software interfaces — ECMAScript® Specification Suite. URL: https://www.iso.org/standard/73002.html
[JOHNNY-FIVE]
Johnny-Five: The JavaScript Robotics & IoT Platform. URL: http://johnny-five.io/
[MDN]
Mozilla Developer Network. URL: https://developer.mozilla.org/en-US/
[NODEJS]
Node.js. URL: https://nodejs.org/
[TC39]
TC39 - ECMAScript. URL: https://www.ecma-international.org/memento/tc39.htm
[WAT]
Gary Bernhardt. Wat. URL: https://www.destroyallsoftware.com/talks/wat
[WHATISMYBROWSER]
What browser am I using?. URL: https://www.whatsmybrowser.org/
[XKCD-703]
Randall Munroe. xkcd: Honor Societies. URL: https://www.xkcd.com/703/
[YDKJS]
Kyle Simpson. You Don't Know JS (book series). URL: https://github.com/getify/You-Dont-Know-JS

Issues Index

To be written. <https://github.com/TimothyGu/es-howto/issues/2>