Symbol.species に未来はあるのか
いや、ない(反語)
@@species
について
ES2015 から導入された ES2015 からクラス構文が導入され、ビルトインクラスを継承したクラスを簡単に作れるようになりました。
class MyArray extends Array {}
ES2015 を策定するにあたって議論となったのがビルトインクラスそのものを返すメソッドの存在です。例えばこの MyArray
のインスタンスで Array#map
を実行したときにその返り値は MyArray
であるべきでしょうか Array
であるべきでしょうか。そしてそれをどうやって Array#map
に伝えればいいでしょうか。
これを制御できるようにするのが @@species
という Well-known Symbol です。普通は派生クラスを返しますが、任意のクラスを返すように変更することも可能になります。
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 において、ビルトインクラスのメソッドから派生クラスを返すのを辞める提案が出されました。
モチベーションとしては
- 仕様が複雑になってしまいメンテナンス性にかける
- パフォーマンスが悪くなる
- セキュリティバグを引き起こす原因になってしまっている[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 のラベルが表示されないというものは影響が小さかったため無視されました。
Array#at
(ES2022) によってサイトが壊れる例が見つかりましたが、報告されたのがこの一例だけだったため無視されました。
この変更には core-js の作者である zloirock 氏が強く反対しています[6]。私も個人的にやりすぎなのではないかと思っています。
しかし今回も統計的に問題がないと判断されれば取り入れられることでしょう。
新しい提案への影響
現在 Stage 2 Change Array by Copy という提案があります。
この提案は Array#reverse
や Array#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
を無視してビルトインクラスを返すようになっています。
もし互換性の問題により Stage 1 Restricting subclassing support in built-in methods が却下されたとしても、今後の提案についてはビルトインクラスのメソッドから派生クラスを生成する仕組みは取り入れられないことになりそうです。
結び
@@species
は仕様から消えるか、それとも仮に生き残ったとしても新しいメソッドでは適用されないか。どちらにせよ未来はなさそうです。
JavaScript の言語仕様には一貫性がないことがよくあります。ES5 からある Array#indexOf
は NaN
を検知できませんが ES2015 Array#includes
では検知できるなどは結構有名なのではないでしょうか。
const arr = [1, 2, 3, NaN];
console.log(arr.indexOf(NaN)); // => -1
console.log(arr.includes(NaN)); // => true
こういった話は JavaScript の面白いところでもあり嫌われるところでもあるのかなと思います。ECMAScript の提案を追うのは割と楽しいので皆さんもよかったらどうぞ。
おまけ
ブラウザの実装から @@species
への対応が取り除かれると何も表示されなくなる行儀の悪いページを作ってみました。壊れるのが楽しみですね。
-
これらのメソッドは仕様の中で ArraySpeciesCreate を使っています。他のビルトインクラスでは SpeciesConstructor を使っているものを探すと見つけられるでしょう。 ↩︎
-
https://docs.google.com/presentation/d/11fkQeEisoszNGF8SrautVT1ltSnsQBWRxJ4usoc-g_o/edit ↩︎
-
この例はもっとも Web 互換性を破壊する恐れのある例です。この提案では派生クラスへの対応を取り除くレベルが考慮されていて、調査によっては派生クラスから同じ派生クラスは作れるものの
@@species
はチェックしないといった弱い変更になるかもしれません。 ↩︎ -
各提案について Chrome チームによって既存サイトへの影響の大きさを統計で出して、それを元に取り入れるかどうか判断されています。影響があっても少数なら無視されることが多いです。 ↩︎
-
Web の互換性よりもユーザーの安全の方が大事です。
@@species
の複雑性によってブラウザにいくつものセキュリティバグを埋め込んでしまっていることを考えるとこの提案は妥当ではあります。 ↩︎ -
私は彼以上に互換性に熱心な方を知りません。compat-table では後から実装に埋め込められてしまったバグをトラッキングできないなどの理由から独自で core-js に互換性テーブルを導入したときにはそこまでやるのかと度肝を抜かれました。 ↩︎
Discussion
Stage 2 Array Grouping からも
Symbol.species
の対応が消えました。