🌐

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

2021/09/20に公開

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.jsDeno でも Web 互換性を重視していることもあって扱うことが出来ます。

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

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

TypeScript の型定義

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

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

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

VSCode を使っている場合打ち消し線が表示されるようになります。また eslint-plugin-deprecation を使うことで eslint でチェック出来るようになります。

Stage 3 Legacy RegExp features

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

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

この型定義もさきほどの PR で @deprecated 扱いになっている[1]ので、気づきやすくなるでしょう[2]

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

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

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

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

結局 browser-compat-data と Web IDL を対応付けるマッパーを用意することで解決されたみたいです。

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

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

@types/web

TypeScript パッケージに含まれているブラウザの型定義だと、TypeScript のバージョンが上がらないことには更新されないということで @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]] を定義することが出来ます。

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]] を設定してしまうのを防いでくれます。

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

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

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

// この場合もただの __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]] に対するゲッター、セッターが定義されています。コアで定義されてはいますが、相変わらずオプショナルでレガシーな機能という扱いに変わりはありません。

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

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

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

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

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

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

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

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

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

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

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" を返すものがあった[4]などのことから typeof 演算子の結果で implementation-defined を許容していたのをもう必要ないとして辞めただとか。

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

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

脚注
  1. 後で気づいてしれっとぶち込みました。 ↩︎

  2. とはいえ RegExp のコンストラクタのプロパティに関するものなので、誤って使うことはない気がします。 ↩︎

  3. もっと厳格な Reflect.{get, set}PrototypeOf を使ってもいいと思います。 ↩︎

  4. https://github.com/tc39/ecma262/issues/1440#issuecomment-461963872 ↩︎

Discussion