Symbol.species に未来はあるのか

いや、ない（反語）

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

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

```js
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

```js
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]
```

```js
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` の影響を受けます[^species-constructor]。

[^species-constructor]: これらのメソッドは仕様の中で [ArraySpeciesCreate](https://262.ecma-international.org/12.0/#sec-arrayspeciescreate) を使っています。他のビルトインクラスでは [SpeciesConstructor](https://262.ecma-international.org/12.0/#sec-speciesconstructor) を使っているものを探すと見つけられるでしょう。

* `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

モチベーションとしては

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

[^security-bugs]: https://docs.google.com/presentation/d/11fkQeEisoszNGF8SrautVT1ltSnsQBWRxJ4usoc-g_o/edit

ということです。

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

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

```js
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 互換性は大切ですが、影響が小さければ破壊的変更を取り入れることがあります[^stat][^safety]。

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

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

具体的には `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](https://github.com/zloirock/core-js) の作者である zloirock 氏が強く反対しています[^zloirock]。私も個人的にやりすぎなのではないかと思っています。

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

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#reverse` や `Array#sort` などといった既存のメソッドと対応して、非破壊的に新しい `Array` を作るものとなっています。

```js
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#indexOf` は `NaN` を検知できませんが ES2015 `Array#includes` では検知できるなどは結構有名なのではないでしょうか。

```js
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/