👋

Symbol.species に未来はあるのか

2021/09/20に公開
1

いや、ない(反語)

ES2015 から導入された @@species について

ES2015 からクラス構文が導入され、ビルトインクラスを継承したクラスを簡単に作れるようになりました。

class MyArray extends Array {}

ES2015 を策定するにあたって議論となったのがビルトインクラスそのものを返すメソッドの存在です。例えばこの MyArray のインスタンスで Array#map を実行したときにその返り値は MyArray であるべきでしょうか Array であるべきでしょうか。そしてそれをどうやって Array#map に伝えればいいでしょうか。

これを制御できるようにするのが @@species という Well-known Symbol です。普通は派生クラスを返しますが、任意のクラスを返すように変更することも可能になります。

https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Symbol/species

class MyArray extends Array {
  // 明示的に @@species を指定しない場合は自身(派生クラス)を返す
  // static get [Symbol.species]() { return this; }
}

const mapped = new MyArray(1, 2, 3).map((val) => val * 2);
console.log(mapped); // => MyArray(3) [2, 4, 6]
class MyArray extends Array {
  // @@species を上書きする
  static get [Symbol.species]() { return Array; }
}

const mapped = new MyArray(1, 2, 3).map((val) => val * 2);
console.log(mapped); // => Array(3) [2, 4, 6]

需要があるのかどうかはおいておいて、とりあえず ES2015 以降の言語仕様にはこの機能があります。

Array で具体的に列挙すると以下のメソッドたちが @@species の影響を受けます[1]

  • Array#concat
  • Array#filter
  • Array#map
  • Array#slice
  • Array#splice
  • Array#flat(ES2019)
  • Array#flatMap(ES2019)

Stage 1 Restricting subclassing support in built-in methods

2020年6月の TC39 meeting において、ビルトインクラスのメソッドから派生クラスを返すのを辞める提案が出されました。

https://github.com/tc39/proposal-rm-builtin-subclassing

モチベーションとしては

  • 仕様が複雑になってしまいメンテナンス性にかける
  • パフォーマンスが悪くなる
  • セキュリティバグを引き起こす原因になってしまっている[2]

ということです。

もしこれが承認された場合、派生クラスから Array#map を呼んだとしても Array 以外を返さなくなります[3]

class MyArray extends Array {}

const mapped = new MyArray(1, 2, 3).map((val) => val * 2);
console.log(mapped); // => Array(3) [2, 4, 6]

Web 互換性の問題

もちろんこの変更は Web 互換性を破壊します。ただし機能が機能なだけに影響は小さいだろうと見積もられています。Web 互換性は大切ですが、影響が小さければ破壊的変更を取り入れることがあります[4][5]

具体的には Array#flat (ES2019) によって Highcharts.js のラベルが表示されないというものは影響が小さかったため無視されました。

https://bugs.chromium.org/p/chromium/issues/detail?id=888128

Array#at (ES2022) によってサイトが壊れる例が見つかりましたが、報告されたのがこの一例だけだったため無視されました。

https://github.com/tc39/proposal-relative-indexing-method/issues/41

この変更には core-js の作者である zloirock 氏が強く反対しています[6]。私も個人的にやりすぎなのではないかと思っています。

https://github.com/tc39/proposal-rm-builtin-subclassing/issues/23

しかし今回も統計的に問題がないと判断されれば取り入れられることでしょう。

新しい提案への影響

現在 Stage 2 Change Array by Copy という提案があります。

https://github.com/tc39/proposal-change-array-by-copy

この提案は Array#reverseArray#sort などといった既存のメソッドと対応して、非破壊的に新しい Array を作るものとなっています。

const arr = [1, 2, 3];
const rev = arr.withReversed(); // メソッド名についてはまだ議論中です
console.log(arr); // => Array(3) [1, 2, 3]
console.log(rev); // => Array(3) [3, 2, 1]

先程の Stage 1 Restricting subclassing support in built-in methods の議論を受けて、現状この提案で追加されるメソッドでは意図的に @@species を無視してビルトインクラスを返すようになっています。

https://github.com/tc39/proposal-change-array-by-copy/pull/38

もし互換性の問題により Stage 1 Restricting subclassing support in built-in methods が却下されたとしても、今後の提案についてはビルトインクラスのメソッドから派生クラスを生成する仕組みは取り入れられないことになりそうです。

結び

@@species は仕様から消えるか、それとも仮に生き残ったとしても新しいメソッドでは適用されないか。どちらにせよ未来はなさそうです。

JavaScript の言語仕様には一貫性がないことがよくあります。ES5 からある Array#indexOfNaN を検知できませんが ES2015 Array#includes では検知できるなどは結構有名なのではないでしょうか。

const arr = [1, 2, 3, NaN];
console.log(arr.indexOf(NaN)); // => -1
console.log(arr.includes(NaN)); // => true

こういった話は JavaScript の面白いところでもあり嫌われるところでもあるのかなと思います。ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。

おまけ

ブラウザの実装から @@species への対応が取り除かれると何も表示されなくなる行儀の悪いページを作ってみました。壊れるのが楽しみですね。

https://petamoriken.github.io/shogyo-mujo/

脚注
  1. これらのメソッドは仕様の中で ArraySpeciesCreate を使っています。他のビルトインクラスでは SpeciesConstructor を使っているものを探すと見つけられるでしょう。 ↩︎

  2. https://docs.google.com/presentation/d/11fkQeEisoszNGF8SrautVT1ltSnsQBWRxJ4usoc-g_o/edit ↩︎

  3. この例はもっとも Web 互換性を破壊する恐れのある例です。この提案では派生クラスへの対応を取り除くレベルが考慮されていて、調査によっては派生クラスから同じ派生クラスは作れるものの @@species はチェックしないといった弱い変更になるかもしれません。 ↩︎

  4. 各提案について Chrome チームによって既存サイトへの影響の大きさを統計で出して、それを元に取り入れるかどうか判断されています。影響があっても少数なら無視されることが多いです。 ↩︎

  5. Web の互換性よりもユーザーの安全の方が大事です。@@species の複雑性によってブラウザにいくつものセキュリティバグを埋め込んでしまっていることを考えるとこの提案は妥当ではあります。 ↩︎

  6. 私は彼以上に互換性に熱心な方を知りません。compat-table では後から実装に埋め込められてしまったバグをトラッキングできないなどの理由から独自で core-js に互換性テーブルを導入したときにはそこまでやるのかと度肝を抜かれました。 ↩︎

Discussion