ECMAScript Annex B と型定義、ついでに ES2022 __proto__
Annex B について
JavaScript の言語仕様には Annex B という項目があります。ここには Web 互換性のために残されているレガシーな機能の仕様について記述してあり、新たに ECMAScript のコードを書く際にこれらの機能を使用したり、その存在を前提にしたりしてはいけないと明記されています。
String#big
などの今となっては全く実用性のないメソッドや、escape
, unescape
函数、もともと IE の独自実装だった String#substr
などについて記述されています。
ブラウザではこれらの機能を取り除くことが出来ないので残念ながら扱うことが出来ます。また Chrome の JavaScript エンジンである V8 を使っている Node.js や Deno でも Web 互換性を重視していることもあって扱うことが出来ます。
一方で Web 互換性を重視しない IoT デバイス向けの JavaScript エンジンである Moddable XS では意図的に一部実装されていません。
TypeScript の型定義
仕様に使うべきではないと明記されている以上 Annex B の機能は避けるべきですが、知らないうちに使ってしまっているかもしれません。
TypeScript 4.5 から Annex B で定義されているものについては型定義ファイルに @deprecated
のアノテーションが付くため、気づきやすくなるかもしれません。
VSCode を使っている場合打ち消し線が表示されるようになります。また eslint-plugin-deprecation を使うことで eslint でチェック出来るようになります。
Stage 3 Legacy RegExp features
実はまだ Annex B にすら記述されていない正規表現のレガシー機能があります。それらを Annex B に追記する提案が Stage 3 Legacy RegExp features です。
この型定義もさきほどの PR で @deprecated
扱いになっている[1]ので、気づきやすくなるでしょう[2]。
【余談】 TypeScript の型定義とブラウザの機能
ECMAScript の Annex B については TypeScript の型定義ファイルに直接 @deprecated
のアノテーションをつけることが出来ましたが、ブラウザの機能については型定義ファイルが Web IDL から自動生成されているため、どうしたものかと思っていました。
Web IDL 側にアノテーションを付ける issue がたっていて見守っていましたが、反対されてうまく進んでいませんでした。
結局 browser-compat-data と Web IDL を対応付けるマッパーを用意することで解決されたみたいです。
これでブラウザの機能についても @deprecated
アノテーションが付くようになりました。
@types/web
TypeScript パッケージに含まれているブラウザの型定義だと、TypeScript のバージョンが上がらないことには更新されないということで @types/web が導入されました。常に最新の型定義を使うことが出来るようになります。
こちらは TypeScript 4.4 から使用することが出来ます。今後はこっちを使うと良さそうです。
__proto__
ES2022 かつて __proto__
に関する仕様は全て Annex B で定義されていました。しかし ES2022 からは通常のコア仕様に格上げされます。
その中身について説明します。説明しますが使わない方が良いです。
Object Initializer
リテラルによってオブジェクトを初期化する際に内部スロットの [[Prototype]]
を定義することが出来ます。
const obj = { __proto__: { a: 1 } };
console.log(obj.a); // => 1
console.log(Object.getPrototypeOf(obj)); // => { a: 1 }
これはコア機能として定義されているので使う分には問題ないかと思いますが、普通は 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 においてプロトタイプ汚染を利用した攻撃に悪用されてきました。
何故厄介者扱いされているかというと、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 ではユーザーが扱えないようにあらかじめ削除されています。オプショナルなので実装環境側で消してしまっても仕様違反にはなりません。
決して使わないようにしましょう。
TypeScript での扱い
いずれの __proto__
の機能も TypeScript では実装されてはいないようでした。
……というわけで使わないようにしましょう。
結び
JavaScript の言語仕様には歴史的な理由から非推奨の機能が残っていたりします。また仕様策定をするにあたって結構面白い話があったりします。
document.all
のために苦肉の策で Undefined-Like Exotic Objects を定義しようとして、結局 Annex B に [[IsHTMLDDA]]
内部スロットを持つオブジェクトを定義しただとか。
他にも IE10 で "unknown"
を返すものがあった[4]などのことから typeof
演算子の結果で implementation-defined を許容していたのをもう必要ないとして辞めただとか。
ECMAScript を追うのは割と楽しいので皆さんもよかったらどうぞ。
-
とはいえ
RegExp
のコンストラクタのプロパティに関するものなので、誤って使うことはない気がします。 ↩︎ -
もっと厳格な
Reflect.{get, set}PrototypeOf
を使ってもいいと思います。 ↩︎ -
https://github.com/tc39/ecma262/issues/1440#issuecomment-461963872 ↩︎
Discussion