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仕様書は、すべてのJavaScript実装の挙動を定義する権威ある情報源です。ブラウザ[WHATISMYBROWSER]、Node.jsサーバー[NODEJS]、IoTデバイス[JOHNNY-FIVE]など、どこであっても仕様書が基準です。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も同様です

仕様書を読むことで解決できます。

また、悪名高い緩い等価演算子==)が実際にはどのように機能するのか疑問に思うかもしれません(ここで「function」を緩く使っています[WAT])。真面目なソフトウェアエンジニアとしてMDNで調べても、長い説明文が逆に混乱を招きます[MDN]

仕様書を読むことで解決できます。

一方で、JavaScript初心者にはECMAScript仕様書の読解はおすすめしません。JavaScriptが初めてなら、まずはウェブで遊んでみてください!ウェブアプリを作ったり、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はtc39.esで最新の仕様書を管理しています[ECMA-262]

ややこしいのは、TC39が毎年仕様書のスナップショットを撮り、その年のECMAScript言語標準とし、版番号を付けることです。例えば、ECMAScript® 2019 Language Specification (ECMA-262, 第10版)[ECMA-262-2019](ES10やES2019として知られる)は、2019年6月時点のtc39.es[ECMA-262]の内容をアーカイブ用に凍結・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 Completion Record・?と!で説明します。

さらに詳しくは:§5.2.1 抽象操作

2.3. [[This]]とは

時々、[[記法]]が「Let proto be obj.[[Prototype]].」のように使われることがあります。この記法は文脈によって意味が異なりますが、「JavaScriptコードからは観測できない内部プロパティ」を指す理解で十分です。

厳密には3つの意味があり、仕様書の例で説明しますが、今は飛ばしても構いません。

2.3.1. Recordのフィールド

ECMAScript仕様では、Recordという用語を、固定されたキーを持つキー・バリューのマップ(C言語の構造体のようなもの)を指すために使います。Recordの各キー・バリューのペアはフィールドと呼ばれます。Recordは仕様書でのみ登場し、実際のJavaScriptコードでは使われません。そのため、[[記法]]フィールドを参照します。

特に、プロパティ記述子Recordとしてモデル化されており、[[Value]]、[[Writable]]、[[Get]]、[[Set]]、[[Enumerable]]、[[Configurable]]というフィールドを持ちます。IsDataDescriptor抽象操作ではこの記法がよく使われます:

抽象操作IsDataDescriptorがプロパティ記述子 Descに対して呼ばれたとき、以下の手順を実行します:

  1. Descundefinedなら、falseを返す。

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

  3. trueを返す。

Recordのさらに具体的な例は次の§2.4 Completion Record・?と!で説明します。

さらに詳しくは:§6.2.1 List・Record仕様型

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

JavaScriptオブジェクトは、仕様書でデータを保持するために使われる内部スロットを持つことがあります。Recordのフィールド同様、内部スロットもJavaScriptから直接観測できませんが、一部はGoogle ChromeのDevToolsなど実装依存のツールで確認できる場合もあります。そのため、[[記法]]内部スロットを表現します。

内部スロットの詳細は§2.5 JavaScriptオブジェクトで解説します。今は用途を気にせず、次の例だけ覚えておいてください。

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

抽象操作OrdinaryGetPrototypeOfがオブジェクトOに対して呼ばれたとき、以下の手順を実行します:

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

注: オブジェクトの内部スロットRecordのフィールドは見た目は同じですが、記法の前置詞(ドットの前の部分)がオブジェクトかRecordかで区別できます。通常、文脈から判断できます。

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

JavaScriptオブジェクトには、内部メソッドと呼ばれるものもあります。内部スロットと同様に、これらの内部メソッドはJavaScriptから直接観察することはできません。そのため、[[記法]]内部メソッドを表現するのが適切です。

内部メソッドの詳細については§ 2.5 JavaScriptオブジェクトで説明します。今は用途について深く考えなくてもよいですが、次の例に注目してください。

すべてのJavaScript関数は、その関数を実行する内部メソッド[[Call]]を持っています。Call抽象操作では、次の手順が記述されています:

  1. Return ? F.[[Call]](V, argumentsList).

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

注: この[[記法]]の三つ目の意味は、関数呼び出しのような形で見分けることができます。

2.4. Completion Record;?および!

ECMAScript仕様書のすべての実行時意味論は、その結果を報告するCompletion Recordを明示的または暗黙的に返します。このCompletion Recordは、三つのRecordフィールドを持つことができます:

注: 二重括弧はRecordのフィールドを示します。§2.3.1 Recordのフィールドで記法の基本を解説しています。

Completion Record[[Type]]normalの場合、それはnormal completionと呼ばれます。それ以外はすべてabrupt completionとも呼ばれます。

ほとんどの場合、abrupt completionのうち、[[Type]]throwのものだけを扱うことになります。他の三つは特定の構文要素の評価過程でしか登場しません。実際、組み込み関数の定義でこれらが現れることはなく、break/continue/returnは関数の境界を越えて動作しないからです。

さらに詳しくは:§6.2.3 Completion Record仕様型


Completion Recordの定義により、JavaScriptでよく見られるtry-catchによるエラーのバブリングのような挙動は仕様書にはありません。実際、エラー(より厳密にはabrupt completion)は明示的に処理されます。

略記を使わない場合、計算結果かエラーを返す可能性がある抽象操作の普通の呼び出しは、次のような記述になります:

略記なしでthrowされる可能性がある抽象操作を呼ぶ一連の手順:

  1. resultCompletionRecordにAbstractOp()を代入する。

    注: resultCompletionRecordCompletion Recordです。

  2. resultCompletionRecordがabrupt completionなら、それを返す。

    注: ここでは、resultCompletionRecordabrupt completionなら直接返されます。つまり、AbstractOp内でthrowされたエラーはそのまま伝播し、残りの手順は中断されます。

  3. resultresultCompletionRecord.[[Value]]を代入する。

    注: normal completionであることを確認した後、Completion Recordから計算結果を取り出せます。

  4. resultが必要な値です。以降の処理に使えます。

これはC言語の手動エラー処理をなんとなく思い出させるかもしれません:

int result = abstractOp();              // Step 1
if (result < 0)                         // Step 2
  return result;                        // Step 2 (continued)
                                        // Step 3 is unneeded
// func() succeeded; carrying on...     // Step 4

このような定型的な手順を減らすため、ECMAScript仕様書の著者たちはいくつかの略記法を導入しました。ES2016以降は、同じ内容が次のいずれかの方法で書けます:

ReturnIfAbruptを使ったthrowされる可能性がある抽象操作を呼ぶ手順:

  1. resultにAbstractOp()を代入する。

    注: ここでも、前の例の手順1と同様に、resultCompletion Recordです。

  2. ReturnIfAbrupt(result)を実行する。

    注: ReturnIfAbruptはabrupt completionがあればそれを伝播し、resultから[[Value]]を自動で取り出します。

  3. resultが必要な値です。以降の処理に使えます。

あるいは、さらに簡潔に、特別な疑問符(?)記法を使うこともできます:

疑問符(?)略記を使ったthrowされる可能性がある抽象操作を呼ぶ手順:

  1. resultに? AbstractOp()を代入する。

    注: この記法ではCompletion Recordを直接扱うことはありません。?略記がすべて処理してくれるため、resultはすぐに利用可能です。

  2. resultが必要な値です。以降の処理に使えます。


特定のAbstractOp呼び出しでabrupt completionが絶対に返らない場合は、仕様の意図を明確にするために感嘆符(!)が使われます:

感嘆符(!)略記を使った絶対にthrowされない抽象操作を呼ぶ手順:

  1. resultに! AbstractOp()を代入する。

    注: ?はエラーを伝播しますが、!はその呼び出しでabrupt completionが絶対に発生しないことを保証し、万一発生すれば仕様のバグとなります。?と同様、Completion Recordは直接扱いません。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]]内部メソッドも)を追加で持つオブジェクトです。そのため、呼び出し可能オブジェクトとも呼ばれます。

仕様書ではすべてのオブジェクトを二つに分類します:普通のオブジェクト特殊なオブジェクトです。ほとんどのオブジェクトは普通のオブジェクトであり、すべての内部メソッド§9.1 普通のオブジェクトの内部メソッドと内部スロットで定義されているデフォルトのものです。

しかし、ECMAScript仕様書では、これらの内部メソッドのデフォルト実装をオーバーライドできる特殊オブジェクトも定義されています。特殊オブジェクトができることには最低限の制約しかありませんが、一般的には仕様に反しない範囲で、オーバーライドされた内部メソッドはかなり自由な振る舞いが可能です。

Array オブジェクトはこれら特殊なオブジェクトの一つです。lengthプロパティの特別な意味論は、普通のオブジェクトが持つ機能だけでは実現できません。

例えば、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 配列の特殊オブジェクト参照。

ECMAScript仕様書では、他の仕様が独自の特殊なオブジェクトを定義することも認めています。ブラウザがクロスオリジンAPIアクセスに制限を設ける仕組みもこのメカニズムで規定されており(WindowProxy[HTML])、JavaScriptプログラマーがProxyAPIで独自の特殊オブジェクトを作ることもできます。


JavaScriptオブジェクトは、特定の値を保持するための内部スロットも持つことがあります。私は内部スロットをObject.getOwnPropertySymbols()でも見つけられないシンボル名のプロパティのようなものと考えています。普通のオブジェクト特殊なオブジェクト内部スロットを持つことができます。

§2.3.2 JavaScriptオブジェクトの内部スロットでは、ほとんどのオブジェクトが持つ[[Prototype]]という内部スロットについて述べました。(実際には、すべての普通のオブジェクトや、特殊なオブジェクトも含め、Arrayオブジェクトも持っています。)しかし、[[GetPrototypeOf]]という内部メソッドもあり、これについては上で簡単に説明しました。何が違うのでしょうか?

キーワードはほとんどのです。ほとんどのオブジェクトには[[Prototype]]内部スロットがありますが、すべてのオブジェクトは[[GetPrototypeOf]]内部メソッドを実装しています。例えば、Proxyオブジェクトは自身の[[Prototype]]を持たず、その[[GetPrototypeOf]] 内部メソッドは、登録されたhandlerやターゲットのプロトタイプ([[ProxyTarget]]内部スロットに保存)に委譲されます。

このため、オブジェクトを扱う際は、適切な内部メソッドを参照し、内部スロットの値を直接見るのは避けた方がよいです。


オブジェクト・内部メソッド内部スロットの関係性は、古典的なオブジェクト指向的観点で考えることもできます。「オブジェクト」はいくつかの内部メソッドを実装するべきインターフェースのようなものです。普通のオブジェクトはデフォルト実装を提供し、特殊なオブジェクトはこれらを部分的または全部オーバーライドできます。一方、内部スロットはオブジェクトのインスタンス変数のようなもので、そのオブジェクトの実装詳細を表します。

これらの関係は、次のUML図でまとめられています(クリックで拡大):

Boxes denoting concepts and connections between them denoting hierarchy

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値が文字列でない場合の動作についてはあまり記載がありません。

ここで仕様書の出番です! 仕様書の左上の検索ボックスでsubstringと入力すると、§21.1.3.22 String.prototype.substring ( start, end )にたどり着き、そこに関数の正規の仕様が記載されています。

アルゴリズムの手順を読む前に、まず既知のことを考えましょう。str.substring()が通常どのように動作するかは、与えられた文字列の一部を返すという基本は知っているはずです。今分からないのは、this値がundefinedの時の挙動です。なので、this値に関するアルゴリズムの手順を重点的に探します。

幸運にも、String.prototype.substring()のアルゴリズムの最初の手順がまさにthis値を処理しています:

  1. Oに? RequireObjectCoercible(this value)を代入する。

?略記が使われているので、RequireObjectCoercible抽象操作が例外を投げる場合があることが分かります。もし例外を投げないなら!が使われているはずです。実際、もし例外が投げられれば、先程の2つ目の仮説と一致します!期待を込めて、RequireObjectCoercibleの中身を確認します。

RequireObjectCoercible抽象操作は少し特殊です。他の抽象操作とは違い、手順ではなく表で定義されています:

引数の型 結果
Undefined TypeError例外を投げる。
... ...

ともかく、Undefined(今回渡した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 version 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で最も避けるべき機能の一つであり、ほぼすべての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値に変換しています。ここでは?!略記はありませんが、通常Completion Record略記がなければ!と同じ意味になります。つまり、手順1は例外を投げません。

手順2はNewTargetundefinedかどうかを確認します。NewTargetは、ES2015で追加されたnew.targetメタプロパティに相当し、new Boolean()呼び出し(この場合はBoolean)と、Boolean()呼び出し(この場合はundefined)を区別できます。今回は直接呼び出しだけを考えているので、NewTargetは必ずundefinedとなり、アルゴリズムはすぐにbを返します。

つまり、Boolean()newなしで呼ぶ場合、アルゴリズムの最初の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, hint String)を代入する。

  2. ? ToString(primValue)を返す。

String()ToStringが呼ばれる時点で、valueはSymbol以外の値であることが保証されています。ただしObjectの場合は、?が2箇所残っています。リンクを辿ってToPrimitiveなどを見ると、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の長さを持ち、[[Prototype]] 内部スロットの値としてprotoを持つ配列オブジェクトを作成します。protoが指定されていない場合、%ArrayPrototype%現在のレルム)が使用されます。Arrayコンストラクターおよびそのすべてのプロパティがモンキーパッチされておらず、protoが指定されていないか%ArrayPrototype%現在のレルム)の場合、new Array(length)と同等です。

Call ( F, V [ , argumentsList ] ) (仕様)
Construct ( F [ , argumentsList [ , newTarget ] ] ) (仕様)
Get ( O, P ) (仕様)
HasProperty ( O, P ) (仕様)

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

DefinePropertyOrThrow ( O, P, desc ) (仕様)
DeletePropertyOrThrow ( O, P ) (仕様)

それぞれ[[DefineOwnProperty]]および[[Delete]]の内部メソッドを、残りの引数を渡してO上で呼び出し、操作が失敗して内部メソッドfalseを返した場合は例外を投げます。

GetV ( V, P ) (仕様)

Vが必要ならToObject後にGet(V, P)を返します。V[P]と同等です。

HasOwnProperty ( O, P ) (仕様)

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

Invoke ( V, P [ , argumentsList ] ) (仕様)

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

IsArray ( argument ) (仕様)

argumentArray 特殊オブジェクトか、argumentProxy 特殊オブジェクトの場合は、その最も内側の[[ProxyTarget]] 内部スロットArray 特殊オブジェクトかどうかを返します。Array.isArray(argument)と同等です。

IsCallable ( argument ) (仕様)

argument呼び出し可能オブジェクト関数オブジェクト)かどうかを返します。typeof argument === 'function'と同等ですが、document.allは例外です(特殊オブジェクトで特別な挙動を持ちます。詳細は§B.3.7 [[IsHTMLDDA]]内部スロット参照)。

IsConstructor ( argument ) (仕様)

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

ReturnIfAbrupt ( argument ) (仕様)

argumentabrupt completion(例外など)であれば、そのabrupt completionを返し(例外を伝播させる)、そうでなければargumentnormal completionであることを確認し、Completion Recordを展開してargumentargument.[[Value]]に設定します。

詳細は:§2.4 Completion Record・?と!参照。

StringCreate ( value, prototype ) (仕様)

文字列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 ) (仕様)

argumentを指定されたビット数と符号で整数に変換し、丸めて返します。

ToUint8Clamp ( argument ) (仕様)

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

ToNumber ( argument ) (仕様)

argumentをNumber型に強制変換して返します。+argumentと同等です。

ToObject ( argument ) (仕様)

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

ToPrimitive ( input [ , PreferredType ] ) (仕様)

inputをプリミティブ(非オブジェクト)値に強制変換して返します。オプションでPreferredType型ヒントを使います。具体的な意味論はPreferredTypeによって異なります。

ToString ( argument ) (仕様)

argumentをStringに強制変換して返します。`${argument}`と同等です。

注意:String(argument)argument + ''は完全にToStringと同等ではありません。String()はSymbol値をその文字列表現に変換しますが、ToStringはSymbolで例外を投げます。加算演算子は値をStringに変換する際にargument[Symbol.toPrimitive]など他の関数を呼び出す場合があります。
Type ( argument ) (仕様)

argumentを返します。

索引

この仕様で定義されている用語

参考文献で定義されている用語

参考文献

参考文献(情報提供)

[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言語仕様書.URL: https://tc39.es/ecma262/
[ECMA-262-2019]
ECMAScript 2019言語仕様書.URL: https://ecma-international.org/ecma-262/10.0/
[HTML]
Anne van Kesteren; 他.HTML Standard.Living Standard.URL: https://html.spec.whatwg.org/multipage/
[ISO-22275-2018]
ISO/IEC 22275:2018 - 情報技術 — プログラミング言語,その環境,およびシステムソフトウェアインターフェース — 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(書籍シリーズ).URL: https://github.com/getify/You-Dont-Know-JS

課題索引

執筆予定。 <https://github.com/TimothyGu/es-howto/issues/2>