ECMAScript Annex B と型定義、ついでに ES2022 __proto__

## Annex B について

JavaScript の言語仕様には Annex B という項目があります。ここには Web 互換性のために残されているレガシーな機能の仕様について記述してあり、新たに ECMAScript のコードを書く際にこれらの機能を使用したり、その存在を前提にしたりしてはいけないと明記されています。

https://tc39.es/ecma262/#sec-additional-ecmascript-features-for-web-browsers

`String#big` などの今となっては全く実用性のないメソッドや、`escape`, `unescape` 函数、もともと IE の独自実装だった `String#substr` などについて記述されています。

ブラウザではこれらの機能を取り除くことが出来ないので残念ながら扱うことが出来ます。また Chrome の JavaScript エンジンである V8 を使っている [Node.js](https://nodejs.org/ja/) や [Deno](https://deno.land/) でも Web 互換性を重視していることもあって扱うことが出来ます。

一方で Web 互換性を重視しない IoT デバイス向けの JavaScript エンジンである [Moddable XS](https://www.moddable.com/) では意図的に一部実装されていません。

https://github.com/Moddable-OpenSource/moddable/blob/e9742b49d5132f2723d3a5fca45b65cf3818d01a/documentation/xs/XS%20Conformance.md#annex-b

### TypeScript の型定義

仕様に使うべきではないと明記されている以上 Annex B の機能は避けるべきですが、知らないうちに使ってしまっているかもしれません。

TypeScript 4.5 から Annex B で定義されているものについては型定義ファイルに `@deprecated` のアノテーションが付くため、気づきやすくなるかもしれません。

https://github.com/microsoft/TypeScript/issues/43709

VSCode を使っている場合打ち消し線が表示されるようになります。また [eslint-plugin-deprecation](https://github.com/gund/eslint-plugin-deprecation) を使うことで [eslint](https://eslint.org/) でチェック出来るようになります。

### Stage 3 Legacy RegExp features

実はまだ Annex B にすら記述されていない正規表現のレガシー機能があります。それらを Annex B に追記する提案が Stage 3 Legacy RegExp features です。

https://github.com/tc39/proposal-regexp-legacy-features

この型定義もさきほどの PR で `@deprecated` 扱いになっている[^legacy-regexp]ので、気づきやすくなるでしょう[^error]。

[^legacy-regexp]: [後で気づいてしれっとぶち込みました。](https://github.com/microsoft/TypeScript/pull/43710/commits/05ed2f756a6967e97bdda993f696b0335f81d900)

[^error]: とはいえ `RegExp` のコンストラクタのプロパティに関するものなので、誤って使うことはない気がします。

## 【余談】 TypeScript の型定義とブラウザの機能

ECMAScript の Annex B については TypeScript の型定義ファイルに直接 `@deprecated` のアノテーションをつけることが出来ましたが、ブラウザの機能については型定義ファイルが Web IDL から自動生成されているため、どうしたものかと思っていました。

Web IDL 側にアノテーションを付ける issue がたっていて見守っていましたが、反対されてうまく進んでいませんでした。

https://github.com/heycam/webidl/issues/910

結局 [browser-compat-data](https://github.com/mdn/browser-compat-data) と Web IDL を対応付けるマッパーを用意することで解決されたみたいです。

https://github.com/saschanaz/bcd-idl-mapper

これでブラウザの機能についても `@deprecated` アノテーションが付くようになりました。

### `@types/web`

TypeScript パッケージに含まれているブラウザの型定義だと、TypeScript のバージョンが上がらないことには更新されないということで [@types/web](https://www.npmjs.com/package/@types/web) が導入されました。常に最新の型定義を使うことが出来るようになります。

https://twitter.com/orta/status/1408428092201357314

こちらは TypeScript 4.4 から使用することが出来ます。今後はこっちを使うと良さそうです。

## ES2022 `__proto__`

かつて `__proto__` に関する仕様は全て Annex B で定義されていました。しかし ES2022 からは通常のコア仕様に格上げされます。

https://github.com/tc39/ecma262/pull/2125

その中身について説明します。説明しますが使わない方が良いです。

### Object Initializer

リテラルによってオブジェクトを初期化する際に内部スロットの `[[Prototype]]` を定義することが出来ます。

```js
const obj = { __proto__: { a: 1 } };

console.log(obj.a); // => 1
console.log(Object.getPrototypeOf(obj)); // => { a: 1 }
```

https://tc39.es/ecma262/#sec-runtime-semantics-propertydefinitionevaluation

これはコア機能として定義されているので使う分には問題ないかと思いますが、普通は `Object.create` を使うところだと思います。お勧めはしません。

またこの仕様の面白いところとして `__proto__` が Computed Property Key の場合は普通のプロパティとして扱われるというのがあります。これによって意図せずに `[[Prototype]]` を設定してしまうのを防いでくれます。

```js
// この場合はただの __proto__ プロパティを持つオブジェクト
const obj = { ["__proto__"]: { a: 1 } };

console.log(obj.a); // => undefined
console.log(Object.getPrototypeOf(obj)); // => Object.prototype
```

`JSON.parse` を使ってオブジェクトを生成した場合も同様です。

```js
// この場合もただの __proto__ プロパティを持つオブジェクト
const obj = JSON.parse("{ \"__proto__\": { \"a\": 1 } }");

console.log(obj.a); // => undefined
console.log(Object.getPrototypeOf(obj)); // => Object.prototype
```

### `Object#__proto__`

`Object#__proto__` に内部スロットの `[[Prototype]]` に対するゲッター、セッターが定義されています。**コアで定義されてはいますが、相変わらずオプショナルでレガシーな機能という扱いに変わりはありません。**

```js
const obj = {};
obj.__proto__ = { a: 1 };

console.log(obj.a); // => 1
console.log(Object.getPrototypeOf(obj)); // => { a: 1 }
```

https://tc39.es/ecma262/#sec-object.prototype.__proto__

この機能は仕様にオプショナルと明記されている以上使うべきではありません。代わりに ES2015 `Object.{get, set}PrototypeOf` を使ったほうが良いです[^reflect]。

[^reflect]: もっと厳格な `Reflect.{get, set}PrototypeOf` を使ってもいいと思います。

またこの機能は（特にセッターが）害悪な存在として有名です。**たびたび Node.js においてプロトタイプ汚染を利用した攻撃に悪用されてきました。**

https://techblog.securesky-tech.com/entry/2018/10/31/

何故厄介者扱いされているかというと、Object Initializer の場合とは異なり、機能の発動が制御できないからです。歴史的によく使われてきた機能ではありますが、今となっては百害あって一利なしです。

```js
// ただの __proto__ プロパティを持つオブジェクト
const base = { ["__proto__"]: { a: 1 } };

// 意図せず Object#__proto__ セッターが実行されてしまう
const obj = Object.assign({}, base);

console.log(obj.a); // => １
console.log(Object.getPrototypeOf(obj)); // => { a: 1 }
```

可能ならトップレベルで削除するのがよいかと思います。 

```js
delete Object.prototype.__proto__;
```

Node.js では `--disable-proto` オプションで制御することができ、Deno ではユーザーが扱えないようにあらかじめ削除されています。オプショナルなので実装環境側で消してしまっても仕様違反にはなりません。

https://nodejs.org/api/cli.html#cli_disable_proto_mode

https://github.com/denoland/deno/pull/4341

決して使わないようにしましょう。

### TypeScript での扱い

いずれの `__proto__` の機能も TypeScript では実装されてはいないようでした。

……というわけで使わないようにしましょう。

## 結び

JavaScript の言語仕様には歴史的な理由から非推奨の機能が残っていたりします。また仕様策定をするにあたって結構面白い話があったりします。

`document.all` のために苦肉の策で Undefined-Like Exotic Objects を定義しようとして、結局 Annex B に `[[IsHTMLDDA]]` 内部スロットを持つオブジェクトを定義しただとか。

https://github.com/tc39/ecma262/pull/673

他にも IE10 で `"unknown"` を返すものがあった[^ie10]などのことから `typeof` 演算子の結果で implementation-defined を許容していたのをもう必要ないとして辞めただとか。

[^ie10]: https://github.com/tc39/ecma262/issues/1440#issuecomment-461963872

https://github.com/tc39/ecma262/pull/1441

ECMAScript を追うのは割と楽しいので皆さんもよかったらどうぞ。