🕸️

Annex Bを読む、の補足と附録

2022/01/16に公開

ECMAScript Annex Bおよび関連する仕様を読みます。「JavaScriptのレガシー挙動を定めたAnnex Bを読む」の記事の文字数制限のため、一部内容を切り出したのが本記事です。

ECMAScript Annex B とは

ECMAScript Annex B ではWebブラウザの互換性のための追加機能を定義しています。これが本文に対するパッチの形で書かれているのは、非推奨で筋の悪い機能でありながらも過去のWebページとの互換性のために残す必要があるからです。そのためAnnex Bはフルの標準ではなく、以下のようなものとして扱われるべきです。

  • WebブラウザはAnnex Bを実装する必要がある。
  • Webブラウザ以外のJavaScriptの処理系はAnnex Bを実装しないのが望ましい。
  • JavaScriptのプログラムを新規に記述したり出力したりする場合はAnnex Bの機能に依存しないのが望ましい。

WHATWG Standardsとの関係

一般的にJavaScriptの機能と考えられているもののうちWebブラウザに固有の部分は、HTMLをはじめとするWHATWGの標準で定義されています。

独立したAPIであればそれでもよいのですが、プログラミング言語の構文や意味論、組み込みクラスにかかわる部分ではそうもいきません。こういった拡張は過去にはWHATWG JavaScriptとしてECMAScriptへの拡張仕様として定義されていましたが、これらの大部分はECMAScriptのAnnex Bに吸収され、WHATWG JavaScriptは役目を終えました。 (リポジトリはアーカイブされています)

LEGACYフラグとNORMATIVE OPTIONALフラグについて

現在Annex Bは大規模な整理が進められています[1]。ES2021でNORMATIVE OPTIONALフラグが、ECMAScript最新版 (2022ドラフト相当) でLEGACYフラグが導入されました。これらは規格本文の一部を修飾する形でつけられます。これらのフラグはConformanceセクションで説明されています。

  • NORMATIVE OPTIONALフラグがついた部分は実装しなくてもかまいませんが、実装する場合はそのブロックを全部実装する必要があります。
  • LEGACYフラグがついた部分は、処理系の実装者は (NORMATIVE OPTIONALでない限りは) 実装する必要がある機能です。新しくJavaScriptのプログラムを書いたり出力する場合は、この機能に依存するべきではありません。

実態に即しているかは別として、これらの説明文からは以下のような内容が読み取れます。

Webブラウザ Webブラウザ以外の処理系 JavaScriptプログラマ
NORMATIVE OPTIONAL 実装しなくてよい 実装しなくてよい
LEGACY 実装する必要がある 実装する必要がある 使うべきではない
Annex B 実装する必要がある 実装しなくてよい 使うべきではない
NORMATIVE OPTIONAL, LEGACY 実装しなくてよい 実装しなくてよい 使うべきではない

そのため、NORMATIVE OPTIONAL, LEGACY 化された機能はAnnex Bからの格下げ、LEGACY化された機能はAnnex Bからの格上げと解釈するのがいいのかもしれません。

NORMATIVE OPTIONALが単独でつけられている場合は非推奨機能ではないので本稿では扱いません。現在は WeakRef.prototype.constructor が唯一の利用例になっています。

歴史

ECMAScriptの過去の仕様書が公開されているので、それに基づいて記述しています。

「新規」と書いてあるものは、前のバージョンの本文にも存在しなかったものです。これは実際のWebブラウザの実装を反映する形で導入されたものだと考えられます。

  • ES1 (1997年): Annex Bはないが、いくつか同等の記述がある。
    • getYear/setYear: 注釈つきで本文中に存在。
    • toGMTString: 注釈つきで本文中に存在。
  • ES2 (1998年) -- 目立った変更はない。
  • ES3 (1999年): Annex Bがはじめて登場。
    • 追加: レガシー8進法リテラル (本文から移動)
    • 追加: 文字列の8進エスケープ (本文から移動)
    • 追加: escape/unescape (本文から移動)
    • 追加: substr (新規)
    • 追加: getYear/setYear (本文から移動)
    • 追加: toGMTString (本文から移動)
  • ES5 (2009年)
    • 変更: レガシー8進法リテラル (strict modeでの禁止規定を追加)
    • 変更: 文字列の8進エスケープ (strict modeでの禁止規定を追加)
  • ES5.1 (2011年) -- 変更なし
  • ES2015 (ES6)
    • 追加: レガシー非8進形式10進法リテラル (新規)
    • 追加: HTML互換コメント <!--, --> (新規; WHATWG JavaScriptからの移管)
    • 追加: 正規表現: 先読みの量化 (新規)
    • 追加: 正規表現: 括弧のフォールバック (新規)
    • 追加: 正規表現: 未知エスケープのフォールバック (新規)
    • 追加: 正規表現: キャプチャグループ番号のフォールバック (新規; WHATWG JavaScriptからの移管)
    • 追加: 正規表現: 無効な範囲指定のフォールバック (新規)
      • 現在とは異なる形で定義されていた。
    • 追加: 正規表現の上書きコンパイルAPI RegExp.prototype.compile (新規)
    • 追加: __proto__ プロパティ (新規)
    • 追加: __proto__ プロパティ初期化子 (新規)
    • 追加: HTML生成関数 anchor, big, blink, bold, fixed, fontcolor, fontsize, italics, link, small, strike, sub, sup (新規; WHATWG JavaScriptからの移管)
    • 追加: ラベルつき関数宣言 (新規)
      • ラベルつき関数宣言を明示的に禁止する本文規定とともに導入。
    • 追加: ブロックレベル関数宣言の互換意味論 (新規)
      • ブロックレベル関数宣言の標準動作を規定する本文規定とともに導入。
    • 追加: if-function (新規)
      • ブロックレベル関数宣言の標準動作を規定する本文規定とともに導入。
    • 追加: catch変数の重複判定の緩和 (新規)
      • デフォルトでcatch変数とcatch節の変数宣言の重複を禁止する本文規定 (非互換な変更) とともに導入。
  • ES2016
    • 追加: 正規表現: 文字クラス内の制御文字エスケープの拡張 (新規)
    • 変更: 正規表現: 無効な範囲指定のフォールバック (構文解析中ではなく意味論でフォールバックを定義するように変更)
  • ES2017
    • 追加: for-inループ中の変数初期化子 (新規)
  • ES2018
    • 追加: 正規表現: キャプチャグループ名のフォールバック
      • 名前つきキャプチャグループとともに導入。
    • 追加: __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__ (新規)
    • 追加: document.all (新規)
  • ES2019
    • 追加: trimLeft / trimRight
  • ES2020 -- 変更なし
  • ES2021
    • NORMATIVE OPTIONAL が導入された。
    • 追加: 文字列の非8進形式10進エスケープ (\8, \9) (新規)
  • ECMAScript 最新版 (2022/01/03時点)
    • NORMATIVE OPTIONALに続き、LEGACYが導入された。
    • 削除: レガシー8進法リテラル、レガシー非8進形式10進法リテラル、文字列の8進エスケープ、文字列の非8進形式10進エスケープ
      • 本文の規定として "LEGACY" フラグつきで取り込まれた。
    • 削除: __proto__ プロパティ、 __proto__ プロパティ初期化子, __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__
      • 本文の規定として "NORMATIVE OPTIONAL", "LEGACY" フラグつきで取り込まれた。
    • 削除: __proto__ プロパティ初期化子
      • 本文の真正な規定として取り込まれた。

ConformanceとForbidden Extensions

Annex BはWebブラウザが規格に対する拡張として実装したものの一部が取り込まれたものですが、その「規格に対する拡張」として許されている範囲については§2. Conformance§17. Error Handling and Language Extensionsである程度規定されています。

まず§2内で、原則として以下の拡張をしてよいと記述されています。

  • ECMAScriptで定義されていない型、値、オブジェクト、プロパティ、関数を追加すること。
  • ECMAScriptで定義されていない構文 (プログラムソースまたは正規表現) を追加すること。

しかし、こういった拡張のうち特定のパターンは§17.1で特別に禁止されています。

  • strict modeが有効な関数の関数オブジェクトや、 function 宣言・ function 式以外の方法で作られた関数の関数オブジェクトに caller プロパティや arguments プロパティを追加してはいけない。
  • 上記の例外として許されているケースでも、 caller プロパティ (←呼び出し元関数を入れることが想定されている) にstrict modeが有効な関数の関数オブジェクトを入れてはいけない。 (つまり、strict modeから呼び出したときは caller プロパティを設定しないという挙動が期待されている)
  • arguments オブジェクトに caller プロパティを追加してはいけない。 (arguments.callee とは別なので注意)
  • ECMA-402 (ECMAScript国際化API) に定められている挙動の拡張についてはECMA-402の規定に従う必要がある。
  • Unicodeモードの正規表現で \y など未知の英字エスケープの IdentityEscape へのフォールバックを実装してはいけない。
  • BindingIdentifier: が後続するような文法を追加してはいけない。
    • TypeScriptやFlowなどで使われている構文を意図的に予約している。 (→ 2014-09-25, 2014-11-19)
  • strict modeでレガシー8進リテラル (017 等) と8進形式10進リテラル (018 等) を許可してはいけない。
  • テンプレートリテラルのエスケープに8進エスケープ (\017 等) と8進形式10進エスケープ (\8 等) を追加してはいけない。
  • Annex Bで規定されている以下の構文をstrict modeでも許すように拡張してはいけない。
    • ラベルつき関数宣言
    • ブロックレベル関数宣言の互換意味論
    • if直下の関数宣言
    • for-inの初期化子
  • Annex Bで規定されているHTML風コメントをモジュールファイル内でもパースするように拡張してはいけない。
  • ImportCall (動的インポートの構文) を拡張してはいけない。

WHATWG JavaScriptに存在したその他の規定

WHATWG JavaScriptに存在した規定のうち、現時点でAnnex Bに入ってないものや、ECMAScriptの本文規定に直接取り込まれたものについて説明します。

トップレベルvar文の振舞いの修正

スクリプトトップレベルで varfunction を使って変数・関数を定義すると、グローバルオブジェクトのプロパティとして定義されます。2回定義すると後の定義が有効になります。

<script>
function foo() {}
</script>
<script>
// 後に定義されたほうが有効
function foo() {}
</script>

上のように、 window.foo が自身のconfigurable, enumerable, writableなデータプロパティとして定義されていた場合はシンプルです。しかし、より一般的に、同名のプロパティが定義されていた場合はどうでしょうか。

<script>
// 先んじてプロパティを定義してしまう
Object.defineProperty(window, "foo", { ... });
</script>
<script>
// 別のscriptタグになっているので、↑のdefinePropertyより後に実行される
// このとき window.foo にはどのようなプロパティが入っている?
function foo() {}
</script>

実は、この場合の細かい挙動についてECMAScriptのバージョンによって少しずつ実装が違います。この部分を実態に合わせつつ合理的な形に修正する一連の作業がAllen Wirfs-Brock氏によって行われており、その過程でWHATWG JavaScriptにこの規定が存在していたようです。

まず最初の議論は2010年 (ES5時点) のこの議論です。ここではES3とES5の間での規定の違いについて議論されています。

  • すでにあるプロパティがアクセサプロパティ (getterまたはsetterを持つプロパティ) だった場合、定義と代入のどちらが発生する?
  • すでにあるプロパティのconfigurable/enumerable属性は上書きされる? non-writableだった場合はどうなる?

ES3 (1999年) ではスクリプトや関数本体の実行前の変数宣言の具現化手順を以下のように説明しています。 (§10.1.3)

  • For each FunctionDeclaration in the code, in source text order, create a property of the variable object whose name is the Identifier in the FunctionDeclaration, whose value is the result returned by creating a Function object as described in 13, and whose attributes are determined by the type of
    code. If the variable object already has a property with this name, replace its value and attributes. Semantically, this step must follow the creation of FormalParameterList properties.
  • For each VariableDeclaration or VariableDeclarationNoIn in the code, create a property of the variable object whose name is the Identifier in the VariableDeclaration or VariableDeclarationNoIn, whose value is undefined and whose attributes are determined by the type of code. If there is already a property of the variable object with the name of a declared variable, the value of the property and its attributes are not changed. Semantically, this step must follow the creation of the FormalParameterList and FunctionDeclaration properties. In particular, if a declared variable has the same name as a declared function or formal parameter, the variable declaration does not disturb the existing property

注意事項として、ES3では全てのスコープの変数管理をオブジェクトを用いて定義していました。たとえば、関数実行時には関数スコープの変数を管理するためのオブジェクトを作り、 varfunction に対応するプロパティをそこに定義します。 (もちろん、実際に処理系がそのように実装する必要はありません。) 規格中の "the variable object" は、このスコープ管理用のオブジェクトを指しています。スクリプトスコープではこれがグローバルオブジェクト (Webブラウザにおける window) になります。

また、ES3ではアクセサプロパティが存在していない点にも注意が必要です。データプロパティの属性は以下のようにほぼ現在のものと同じです。 (§8.6.1)

  • ReadOnly -- ES5のWritable属性の否定に相当。
  • DontEnum -- ES5のEnumerable属性の否定に相当。
  • DontDelete -- ES5のConfigurable属性の否定に相当。
  • Internal -- Internalがついた「プロパティ」はES5以降は内部スロットと呼ばれている。

あらためてES3の記述を読むと、まず var の初期化子はこのステップでは実行されておらず、かわりに undefined で初期化しています。しかし、すでにプロパティが存在しているときは何もしないと定義されています (同名の functionvar があった場合は function が勝つ)。 そのため var の振舞いについては特に難しいところはありません。

一方 function についてはこの位置でプロパティを上書きする規定となっており、その際プロパティの値と属性をどちらも上書きすることが明記されています。

ES5 (2009年) ではこれが以下のようになります。 (§10.5)

  1. For each FunctionDeclaration f in code, in source text order do
    • a. Let fn be the Identifier in FunctionDeclaration f.
    • b. Let fo be the result of instantiating FunctionDeclaration f as described in Clause 13.
    • c. Let funcAlreadyDeclared be the result of calling env’s HasBinding concrete method passing fn as the argument.
    • d. If funcAlreadyDeclared is false, call env’s CreateMutableBinding concrete method passing fn and configurableBindings as the arguments.
    • e. Call env’s SetMutableBinding concrete method passing fn, fo, and strict as the arguments.

ES5ではスコープ (environment record) が必ずスコープ管理用オブジェクトを持つような定義ではなくなったため、environment recordに CreateMutableBinding や SetMutableBinding などの抽象メソッドを定義してそれを呼ぶようになっています。とにかく、要約するとここでは以下のような手順が踏まれています。

  • もし束縛がなければ (HasBinding)、束縛を作成する (CreateMutableBinding)
  • (既存のまたは今作ったばかりの) 束縛に関数オブジェクトを代入する (SetMutableBinding)

さて、ES5ではスクリプト実行時のスコープにはobject environment record (§10.2.1.2) が使われます。object environment recordの定義を使うと、上の手順は以下のようになります。

  • オブジェクトにプロパティがなければ ([[HasProperty]])、データプロパティを作成する ([[DefineOwnProperty]])
    • 値は undefined で属性はwritable=true, enumerable=true, configurable=false。
  • プロパティに関数オブジェクトを代入する ([[Put]])

[[Put]] (現在は [[Set]] と呼ばれている) が使われている点がポイントです。これは = 演算子と同様の代入処理のため、すでにアクセサプロパティがあった場合はセッターが呼ばれる挙動になります。また、既存プロパティの属性は一切変更されません。

この部分はES3の挙動のほうが望ましいだろうということになり、ES5.1 (2011年) では以下のように修正されました。 (§10.5)

  1. For each FunctionDeclaration f in code, in source text order do
    • ...
    • d. If funcAlreadyDeclared is false, call env’s CreateMutableBinding concrete method passing fn and configurableBindings as the arguments.
    • e. Else if _en_v is the environment record component of the global environment then
      • i. Let go be the global object.
      • ii. Let existingProp be the resulting of calling the [[GetProperty]] internal method of go with argument fn.
      • iii. If existingProp.[[Configurable]] is true, then
          1. Call the [[DefineOwnProperty]] internal method of go, passing fn, Property Descriptor {[[Value]]: undefined, [[Writable]]: true, [[Enumerable]]: true , [[Configurable]]: configurableBindings }, and true as arguments.
      • iv. Else if IsAccessorDescriptor(existingProp) or existingProp does not have attribute values {[[Writable]]: true, [[Enumerable]]: true}, then
          1. Throw a TypeError exception.
    • f. Call env’s SetMutableBinding concrete method passing fn, fo, and strict as the arguments.

グローバルスコープの場合の分岐を用意し (※with文のケースは明示的に除外されている)、既存プロパティが存在している場合に追加処理を行っています。

  • 既存プロパティがconfigurable=true属性を持っている場合は、新しいプロパティで置き換える。
  • 既存プロパティがconfigurable=falseでも、writable=true, enumerable=trueのデータプロパティの場合はそのまま進む。
  • それ以外の場合はエラー。

ところがこの定義にもある種の見落としがありました。グローバルオブジェクト自身ではなく、そのプロトタイプチェーン上に既存プロパティが存在している場合まできちんと想定した定義になっていなかったのです。 (#78)

ES5.1の定義を見直すと、プロパティの存在判定は [[GetProperty]] で行っているのに対し、プロパティの作成は [[DefineOwnProperty]] で行っています。 (同じことがES5/ES5.1の var の具現化処理にも成り立ちます。) このせいで、以下のような期待される性質が実際には成り立っていません。

  • スクリプトトップレベルで functionvar を宣言すると、グローバルオブジェクト自身のプロパティが存在する状態になる。
    • 実際には、グローバルオブジェクトのプロトタイプチェーン上にプロパティが存在しているときには作成されない可能性がある。

この問題はES2015で修正されました。ES2015での定義を読む前に、このあたりの定義方法の変更をおさらいしておきます。

  • ES5.1まではグローバルスコープと with 文のスコープを同じobject environment recordとして表現していましたが、ES2015ではグローバルスコープのためにglobal environment recordという専用のenvironment recordが定義されています。global environment recordは内部にobject environment recordとdeclarative environment recordを1つずつ持っていて、操作の多くをこのいずれかに移譲する構成になっています。 varlet/const の振舞いの違いを反映するためにこのような構成になっているのでしょう。
  • ES2015では変数束縛の具現化処理がGlobalDeclarationInstantiation, ModuleDeclarationInstantiation, FunctionDeclarationInstantiation, BlockDeclarationInstantiationのようにスコープごとに分割されました。GlobalDeclarationInstantiation内で function, var に対応して呼ばれる処理は let/const の振舞いと区別するためにCreateGlobalFunctionBinding/CreateGlobalVarBindingとして分けられました。

そのCreateGlobalFunctionBindingはおおよそ以下のような手順になっています。 (§8.1.1.4.18)

  • グローバルオブジェクト自身の既存のプロパティを取得する。 ([[GetOwnProperty]])
  • プロパティの作成または上書きを行う (DefinePropertyOrThrow) 。ただし、
    • 既にconfigurable=falseなプロパティが存在していた場合は、値の上書きだけを試みる。
    • それ以外の場合 (既存プロパティが存在しない、または存在するがconfigurable=trueである) は、 writable=true, enumerable=true の設定も同時に試みる。
  • 作成したプロパティに値を代入する (Set)。
    • これは一見すると余計だが、Proxyのフックを発火するために行われている。

同様に、CreateGlobalVarBindingは以下のようになっています。 (§8.1.1.4.17)

  • グローバルオブジェクト自身の既存のプロパティが存在 (HasOwnProperty) せず、グローバルオブジェクトが拡張可能 (IsExtensible) ならば、内部に持っているobject environment recordのCreateMutableBindingとInitializeBindingに処理を移譲して束縛を作成する。
    • object environment recordのCreateMutableBinding → 自身のプロパティを作成する (DefinePropertyOrThrow)。このとき値は undefined で、属性はwritable=true, enumerable=true, configurable=false になる。
    • object environment recordのInitializeBinding → 初期化済みフラグを立てたあと、 Set でプロパティに値を代入する。

このES2015の修正を先んじて取り込むため、2012年から2016年までWHATWG JavaScriptにこの規定が存在していました。

spliceの振舞いの修正

Array.prototype.splice のWebブラウザ実装は引数の個数に応じて以下のような振舞いの違いが存在します。

[1,2,3].splice(1) // => [2,3]
[1,2,3].splice(1, undefined) // => []

ES5.1までの規定ではこの実装実態が反映されておらず、 undefined を明示したときと同じ挙動で規定されていました。 (ES5.1, §15.4.4.12)

  1. Let actualDeleteCount be min(max(ToInteger(deleteCount),0), lenactualStart).

ここで使われているToInteger (現在はToIntegerOrInfinityと呼ばれている) は Number(x) に対して以下の変換を施します。

ToNumber(x) ToInteger(x)
NaN +0
+0, -0, +∞, -∞ そのまま
それ以外 0方向に丸めた整数

Number(undefined) はNaNのため、 ToIntegerの結果は0になります。第2引数はspliceの削除要素数の指定のため、spliceは何も行わずに [] を返すことになります。

ES2015では引数が1個の場合の特別扱いが明記されました。 (§22.1.3.25)

  1. If the number of actual arguments is 0, then
    • a. Let insertCount be 0.
    • b. Let actualDeleteCount be 0.
  2. Else if the number of actual arguments is 1, then
    • a. Let insertCount be 0.
    • b. Let actualDeleteCount be lenactualStart.
  3. Else,
    • a. Let insertCount be the number of actual arguments minus 2.
    • b. Let dc be ToInteger(deleteCount).
    • c. ReturnIfAbrupt(dc).
    • d. Let actualDeleteCount be min(max(dc,0), lenactualStart)

現在は the number of actual arguments は使わないものの、 "if deleteCount is not present" という文言で同等の分岐を表現しています。

このES2015の修正を先んじて取り込むため、2012年から2014年までWHATWG JavaScriptにこの規定が存在していました。

Date.UTCの振舞いの修正

Date.UTC は通常2つ以上の引数 (年・月) を指定して呼び出します。

Date.UTC の引数が2個未満の場合について、 ES5.1では実装依存と規定しています (§15.9.4.3)。

When the UTC function is called with fewer than two arguments, the behaviour is implementation-dependent.

WHATWG JavaScriptではこの部分の挙動を明確にするため、2012年から2016年まで必ずNaNを返すよう規定していました。

When called with fewer than two arguments, Date.UTC must return NaN.

しかし、Chakra (Microsoft Edge Legacyで使われていたJSエンジン) では異なる挙動になっていることが指摘され、実際にはWeb互換性のために必要な規定ではないだろうということで削除されています。そのChakraは以下のような挙動だったようです。

  1. If year is supplied, let y be ? ToNumber(year); else let y be 0.
  2. If month is supplied, let m be ? ToNumber(month); else let m be 0.
  3. etc.

つまり、省略されたときはいずれも0とみなしていました。

最終的にES2017で挙動が標準化されました。これは以下のような規定で落ち着きました。

When the UTC function is called, the following steps are taken:

  1. Let y be ? ToNumber(year).
  2. If month is present, let m be ? ToNumber(month); else let m be +0<sub>𝔽</sub>.
  3. ...

第2引数である month は省略すると0として扱う一方、 year は省略すると Number(undefined) つまり NaN として扱われます。結果として Date.UTC() の結果はNaNになります。

RegExpのレガシープロパティ

RegExpの RegExp.prototype.compile はAnnex Bに入っていますが、他にもレガシープロパティが存在しています。WHATWG JavaScriptでは以下を規定していました。

  • RegExp.$1, ..., RegExp.$9
  • RegExp.lastMatch, RegExp["$&"]

これらは最後に実行された正規表現の実行結果をグローバルに保持するもので、正規表現の処理に多少手を加える必要があることもあり分けられているようです。現在はregexp-legacy-featuresというStage 3の提案として切り出されています。regexp-legacy-features提案ではさらに以下のプロパティが足されています。

  • RegExp.input, RegExp.$_
  • RegExp.lastParen, RegExp["$+"]
  • RegExp.leftContext, RegExp["$`"]
  • RegExp.rightContext, RegExp["$'"]

これに加えて、 RegExp.prototype.compile を含む正規表現のレガシー機能を「RegExpのサブクラス」および「別レルムのRegExpコンストラクタで作った正規表現」について禁止する変更も含まれているようです。

argumentsプロパティとcallerプロパティ

現在のECMAScriptは Function.prototype.argumentsFunction.prototype.caller を以下のように定義しています。 (当該プロパティの初期化処理の記述)

  1. Let thrower be realm.[[Intrinsics]].[[%ThrowTypeError%]].
  2. Perform ! DefinePropertyOrThrow(F, "caller", PropertyDescriptor { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: true }).
  3. Return ! DefinePropertyOrThrow(F, "arguments", PropertyDescriptor { [[Get]]: thrower, [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: true }).

つまり、これらのプロパティは取得・設定しようとするとTypeErrorが発生します。

Function.prototype.arguments // => (TypeError)
Function.prototype.caller // => (TypeError)

そして、strict modeが有効化された関数の関数オブジェクトにこれらの名前のプロパティを定義することを明示的に禁止しています

  • ECMAScript function objects defined using syntactic constructors in strict mode code must not be created with own properties named "caller" or "arguments".

わざわざこのような定義にするのはstrict mode以外でこれらのプロパティが存在するかもしれないからですが、そのことについては現在のECMAScriptでは言及がありません。それを補完し、実際のWebブラウザの挙動を記述していたのがWHATWG JavaScriptの記述でした。具体的には:

  • 関数オブジェクトの caller プロパティは、その関数が実行中のとき、その呼び出し元関数を返す。
  • 関数オブジェクトの arguments プロパティは、その関数が実行中のとき、その関数が受け取る arguments 変数と同じオブジェクトを返す。

というような挙動が規定されています。

WHATWG JavaScriptが廃止されて以降はtc39/ecma262#562でECMAScriptへの取り込みが議論されています。

関連する機能として arguments.callee がありますが、これはECMAScriptで以下のように規定されています

  • その関数でstrict modeが有効なときは、エラーを返すアクセサプロパティとして定義される。
  • その関数の引数が単純 (変数名の羅列) ではないときも、strict modeが有効なときに準じる。
  • それ以外の場合は、関数オブジェクト自身が代入されたデータプロパティとして定義される。

関数のtoString

function宣言等で生成された関数オブジェクトをtoStringすると、元のソースコードが得られます。これについてES2018まではざっくり以下のように規定されていました。 (ES2018, §19.2.3.5)

  • 可能な場合は、 FunctionDeclaration などの関数形式か MethodDefinition などのメソッド形式の構文でパースできる文字列を返す。特に関数形式で出力する場合、適切なコンテキストでevalすれば元の関数の挙動と等価な関数が復元できるように出力しなければならない。
  • 上記ができない場合は、 evalしてもSyntaxErrorになるような文字列を返す。

このときどのような文字列が返ってくるかについてより詳しく、また実態に沿った形で規定するために、WHATWG JavaScriptにTODOとしてセクションが残されていました (実際に詳細が書かれたことはない) 。その後WHATWG JavaScript廃止後はFunction-prototype-toString-revision 提案として推敲され、ES2019でマージされました。マージ後はおよそ以下のような規定になっています

  • ソースコードをパースして得られた関数の場合:
    • ソースコード中の当該部分に対応する文字列 ([[SourceText]]) をそのまま返す。
    • ただし、元のソースコードを保持するためのメモリを節約したい場合など、ホストがこれを実装しないと決めた場合 (HostHasSourceTextAvailable(func) がfalseの場合) はネイティブ実装と同様の出力をする。
  • ネイティブ実装された関数や Function.prototype.bind 等により束縛された関数等の場合:
    • function() { [native code] } を返す。ただし、スペースの入れ方は規定されていないほか、関数名と引数部分はカスタマイズできる。
    • 一部の関数については関数名部分に入れるべき名前が指定されている。

Webブラウザ以外による実装状況

Annex BはWebブラウザ用の規格ですが、実際にはWebブラウザ以外の環境でも実装されている場合があります。たとえばNode.jsはV8を使っているため、V8に実装されているAnnex Bのサポートがそのまま使える場合があります。Annex Bのいくつかの規定について実装状況を調べてみます。

レガシー8進リテラルとその仲間たち

以下のコードで調べます。

017 // レガシー8進リテラル
018 // 8進形式10進リテラル
"\017" // 8進エスケープ
"\8" // 8進形式10進エスケープ
  • Node.js: ✔️レガシー8進リテラルとその仲間たちは実装されている。
  • TypeScript: ✔️レガシー8進リテラルとその仲間たちは実装されている。
    • alwaysStrictをオフにして、ターゲットをES3にする必要がある。
  • Babel: ✔️レガシー8進リテラルとその仲間たちは実装されている。
    • パーサーの strictMode オプションをオフにする必要がある。

HTMLコメント

HTMLの開きコメントは以下のコードで判別できます。閉じコメントは同様の方法で判別することはできません。ここでは開きコメントの実装状況を調べます。

let x = 1;
if(0<!--x
) {
  console.log("HTML Opening Comment: disabled");
} else {
  console.log("HTML Opening Comment: enabled");
}
  • Node.js: ✔️HTMLコメントは実装されている。
    • モジュールではHTMLコメントを無視するのではなく、パースしてエラーにしている。
  • TypeScript: HTMLコメントは実装されていない。
  • Babel: HTMLコメントは実装されていない。

なお、閉じコメントのサポート状況によって結果が変わるスクリプトは書けませんが、単に以下のようなソースコードのパースが通るかどうか調べればサポート状況はわかります。

-->

正規表現のレガシー文法

TypeScriptやBabelなどのトランスパイラは正規表現の内部のパースをする必要がないため、この機能とは関係ありません。

  • Node.js: ✔️正規表現のレガシー文法は実装されている。

「正規表現のレガシー文法」は正確にはいくつかの文法の集まりですが、単にV8の機能を流用しているだけだと思われるので詳細は省略します。

__proto__, __defineGetter__, __defineSetter__, __lookupGetter__, __lookupSetter__

const obj = {};
obj.__proto__ = { foo: true };
if (obj.foo) {
  console.log("__proto__: enabled");
} else {
  console.log("__proto__: disabled");
}

const obj2 = { __proto__: { foo: true } };
if (obj2.foo) {
  console.log("__proto__ initializer: enabled");
} else {
  console.log("__proto__ initializer: disabled");
}

console.log(`__defineGetter__: ${{}.__defineGetter__ ? "enabled" : "disabled"}`);
console.log(`__defineSetter__: ${{}.__defineSetter__ ? "enabled" : "disabled"}`);
console.log(`__lookupGetter__: ${{}.__lookupGetter__ ? "enabled" : "disabled"}`);
console.log(`__lookupSetter__: ${{}.__lookupSetter__ ? "enabled" : "disabled"}`);

escape/unescape

console.log(`escape: ${typeof escape === "function" ? "enabled" : "disabled"}`);
console.log(`unescape: ${typeof unescape === "function" ? "enabled" : "disabled"}`);
  • Node.js: ✔️ escape/unescape は実装されている。
  • TypeScript: ✔️ escape/unescape の型定義はある。 (deprecated)
  • core-js: ✔️ escape/unescape のpolyfillはある。

substr

console.log(`substr: ${"".substr ? "enabled" : "disabled"}`);
  • Node.js: ✔️ substr は実装されている。
  • TypeScript: ✔️ substr の型定義はある。 (deprecated)
  • core-js: ✔️ substr のpolyfillはある。

HTML生成関数

for(const tag of ["anchor", "big", "blink", "bold", "fixed", "fontcolor", "fontsize", "italics", "link", "small", "strike", "sup", "sub"]) {
  console.log(`${tag}: ${""[tag] ? "enabled" : "disabled"}`);
}

trimLeft/trimRight

console.log(`trimLeft: ${"".trimLeft ? "enabled" : "disabled"}`);
console.log(`trimRight: ${"".trimRight ? "enabled" : "disabled"}`);
  • Node.js: ✔️ trimLeft/trimRight は実装されている。
  • TypeScript: ✔️ trimLeft/trimRight の型定義はある。 (deprecated)
  • core-js: ✔️ trimLeft/trimRight のpolyfillはある。

getYear/setYear/toGMTString

console.log(`getYear: ${new Date().getYear ? "enabled" : "disabled"}`);
console.log(`setYear: ${new Date().setYear ? "enabled" : "disabled"}`);
console.log(`toGMTString: ${new Date().toGMTString ? "enabled" : "disabled"}`);
  • Node.js: ✔️ getYear/setYear/toGMTString は実装されている。
  • TypeScript: getYear/setYear/toGMTString の型定義はない。
  • core-js: ✔️ getYear/setYear/toGMTString のpolyfillはある。

RegExp.prototype.compile

console.log(`RegExp.prototype.compile: ${/x/.compile ? "enabled" : "disabled"}`);
  • Node.js: ✔️ RegExp.prototype.compile は実装されている。
  • TypeScript: ✔️ RegExp.prototype.compile の型定義はある。 (deprecated)
  • core-js: RegExp.prototype.compile のpolyfillはない。

RegExpのレガシープロパティ

for (const name of ["$1", "$2", "$3", "$4", "$5", "$6", "$7", "$8", "$9", "lastMatch", "$&", "input", "$_", "lastParen", "$+", "leftContext", "$`", "rightContext", "$'"]) {
  /(b)(c)(d)(e)(f)(g)(h)(i)(j)/.exec("abcdefghijk");
  const val = RegExp[name];
  console.log(`${name}: ${val ? "enabled" : "disabled"}`);
}
  • Node.js: ✔️ RegExpのレガシープロパティは実装されている。
  • TypeScript: ✔️ RegExpのレガシープロパティの型定義はある。 (deprecated)
  • core-js: RegExpのレガシープロパティのpolyfillはない。

ラベルつき関数宣言

以下のコードがSyntaxErrorになるかどうかでチェックします。

l: function foo() {}
  • Node.js: ✔️ ラベルつき関数宣言は実装されている。
  • TypeScript: ✔️ ラベルつき関数宣言は実装されている。
    • alwaysStrict オプションをオフにする必要がある。
  • Babel: ✔️ ラベルつき関数宣言は実装されている。
    • パーサーの strictMode オプションをオフにする必要がある。

ブロックレベル関数宣言の代替意味論

var foo = function() {
  console.log("Block-level function hoisting: disabled");
};

if (true) {
  function foo() {
    console.log("Block-level function hoisting: enabled");
  }
}

foo();
  • Node.js: ✔️ ブロックレベル関数宣言の代替意味論は実装されている。
  • TypeScript: ✔️ ブロックレベル関数宣言の代替意味論は型チェッカーによって認識されている。
    • alwaysStrict オプションをオフにする必要がある。
    • 上のソースコードを入力するとduplicate identifierとしてエラーになるが、 "use strict"; を付与するとエラーが消える。このことからブロックレベル関数宣言の代替意味論が考慮されていることがわかる。
  • Babel: 不明
    • スコープ追跡が必要な変換を挟むことで実際の挙動を確かめることができるはずだが、そこまで調べきれていない。

if文直下の関数宣言

以下のコードがSyntaxErrorになるかどうかでチェックします。

if (true) function foo() {}
  • Node.js: ✔️ if文直下の関数宣言は実装されている。
  • TypeScript: ✔️ if文直下の関数宣言は実装されている。
    • (本来であれば) alwaysStrict オプションをオフにする必要がある。
    • なぜか、strict modeでもコンパイルエラーにならない。 (現時点での挙動)
  • Babel: ✔️ if文直下の関数宣言は実装されている。
    • パーサーの strictMode オプションをオフにする必要がある。

catch変数の重複判定の緩和

以下のコードがSyntaxErrorになるかどうかでチェックします。

try {
} catch (x) {
  var x;
}
// 以下は通らないことが期待される:
// try {} catch ([x]) { var x; }
  • Node.js: ✔️ catch変数の重複判定の緩和は実装されている。
  • TypeScript: ✔️ catch変数の重複判定の緩和は実装されている。
    • なぜか、catch 内をパターンにしてもコンパイルエラーにならない。 (現時点での挙動)
  • Babel: ✔️ catch変数の重複判定の緩和は実装されている。

for-inの初期化子

以下のコードがSyntaxErrorになるかどうかでチェックします。

for (var x = 0 in {}) {}
  • Node.js: ✔️ for-inの初期化子は実装されている。
  • TypeScript: for-inの初期化子は実装されていない。
  • Babel: ✔for-inの初期化子は実装されていない。

document.all

[[IsHTMLDDA]] 内部スロットを持つオブジェクトは通常の方法で作ることはできないため、jsdomなどのライブラリはdocument.all実装していないと考えられます。もちろん、Node.js自身にも document.all のような振舞いをするオブジェクトを作る動機は存在しません。

TypeScripの型チェッカが document.all の特殊な挙動を認識しているかどうかは以下のようなコードで確かめることができます。

let x: string | null = null;
if (document.all) {} else { x = "foo"; }
// if (false) {} else { x = "foo"; }

console.log(x.charAt(0));
//          ^ Object is possibly 'null'.

予想通り、 document.all がfalsyであることを利用したフロー解析は行われず、型エラーが報告されています。

脚注
  1. Annex Bの整理を進めているMark S. Miller氏についてはPromiseの歴史でも紹介しました。 ↩︎

Discussion