JavaScript/Objekte/Symbol/species
In Symbol.species
finden Sie das well-known Symbol @@species
.
Syntax
Symbol.species
Writable | false |
Enumerable | false |
Configurable | false |
Die eingebauten Klassen Array, ArrayBuffer, Promise, RegExp, Map, Set sowie die TypedArray-Familie definieren auf ihrer jeweiligen Konstruktorfunktion einen Eigenschafts-Getter mit dem Namen @@species. Dieser Getter gibt this
zurück.
Achtung!
Inhaltsverzeichnis
Das Problem
Die zuvor genannten Klassen haben eine Gemeinsamkeit: Sie besitzen Methoden, die eine neue Instanz dieser Klasse zurückgeben können. Beispiele wären
-
filter()
für Arrays -
slice()
für Arrays, ArrayBuffer oder TypedArray-Subklassen -
then()
für Promises - Map und Set besitzen die @@species-Eigenschaft, nutzen sie aber nicht
- RegExp besitzt interne Methoden, die von String-Methoden verwendet werden und die die RegExp kopieren. Auch dafür wird auf @@species zurückgegriffen.
Das Erzeugen einer neuen Instanz ist unproblematisch, wenn direkt mit diesen Klassen gearbeitet wird. Die Schwierigkeit beginnt, wenn man von diesen Klassen Subklassen abgeleitet hat. Die Erwartungshaltung wäre nun, dass die Methoden, die an die Subklasse vererbt werden und die neue Instanzen erzeugen können, Instanzen der Subklasse erzeugen. Aber dafür müssen sie den Konstruktor der Subklasse kennen.
Betrachten wir ein Beispiel mit Arrays und der filter()
-Methode.
class SpecialArray extends Array {
...
}
let specArr = new SpecialArray();
specArr.push(1);
specArr.push(2);
specArr.push(3);
let sf = specArr.filter(elem => elem > 1);
console.log(sf.constructor.name); // Array oder SpecialArray?
Das Beispiel definiert eine Klasse SpecialArray
, die von Array
abgeleitet ist. Warum eine Subklasse erzeugt wird und was sie tut, ist für dieses Beispiel nicht von Bedeutung. Von Bedeutung ist, dass dieses SpecialArray
von Array.prototype
die filter()
-Methode vererbt bekommt.
Unser Beispiel erzeugt ein neues SpecialArray
-Objekt und speichert mit der ebenfalls geerbten push()-Methode drei Zahlen darin. Als nächstes sollen mittels filter()
diejenigen Werte in ein neues SpecialArray
übernommen werden, die größer als eins sind.
Aber wie macht filter()
das? Als diese Methode programmiert wurde, wusste niemand davon, dass es die Subklasse SpecialArray
jemals geben würde!
- Die Konstruktorfunktion einer Subklasse besitzt die Konstruktorfunktion der Elternklasse als Prototyp. Es ist also
Object.getPrototypeOf(SpecialArray) == Array
- und damit erbt dieSpecialArray
-Funktion die Eigenschaft@@species
, die aufArray
definiert ist. - Die
@@species
-Eigenschaft ist mit einer Getter-Funktion realisiert. Ruft manSpecialArray[Symbol.species]
auf, wird diese Getter-Funktion aufgerufen und gibtthis
zurück - und das ist dieSpecialArray
-Konstruktorfunktion. - Jedes Objekt, das durch eine Konstruktorfunktion erzeugt wurde, erbt vom Prototyp-Objekt, das diese Konstruktorfunktion bereitstellt, die Eigenschaft
constructor
, die wieder auf die Konstruktorfunktion verweist. WennspecArr
ein SpecialArray-Objekt ist, dann liefertspecArr.constructor
die Konstruktorfunktion dazu.
Die Lösung
Als die filter()
-Methode programmiert wurde, wusste man aber immerhin, dass es möglicherweise einmal Subklassen von Array
geben könnte. Man wusste auch, dass möglicherweise jemand mittels call()
die filter()
-Methode auf ein Objekt anwenden könnte, das gar kein Array ist, sich aber ähnlich verhält.
Und deshalb verwenden die Klassen, die @@species
unterstützen, nicht einfach einen Aufruf wie new Array()
. Statt dessen beziehen sie sich auf die constructor
-Eigenschaft, die jedes Objekt, das über eine Konstruktorfunktion erzeugt wurde, vom Prototypobjekt dieser Konstruktorfunktion erbt. Diese Eigenschaft verweist vom Prototyp-Objekt auf die Konstruktorfunktion zurück.
In unserem Beispiel würde specArr.constructor
also die SpecialArray-Konstruktorfunktion liefern. In der Implementierung der filter()
-Methode könnte man also bequem mittels
let result = new (this.constructor)();
das Ergebnis der Filterung vorbereiten. Und was ist nun mit @@species
?
@@species - die Lösung für Individualisten
Es gab für die Erfinder von @@species
offenbar Einsatzgebiete, wo die beschriebene Lösung unbefriedigend ist. Was ist, wenn man gar kein SpecialArray
als Ergebnis von filter()
haben möchte, sondern ein normales Array oder etwas ganz anderes?
Dafür kann eine Subklasse den @@species
-Getter ihrer Elternklasse überschreiben und eine Konstruktorfunktion nach eigenem Gusto zurückgeben. Die Syntax für diesen Getter ist etwas obskur:
class SpecialArray2 extends Array {
static get [Symbol.species]() { return Array; }
...
}
let specArr = new SpecialArray2();
specArr.push(1);
specArr.push(2);
specArr.push(3);
let sf = specArr.filter(elem => elem > 2);
console.log(sf.constructor.name); // Array oder SpecialArray2?
Die Zeile
static get [Symbol.species]() { return Array; } 6 5 3 4 1 2
ist eine ziemliche Ladung Code. Sie ist so zu verstehen:
- Es wird eine Funktion definiert
- Die Funktion gibt einen Verweis auf die Array-Konstruktorfunktion zurück
- Die eckigen Klammern besagen: Der Name der Funktion wird aus einer Variablen geholt. Funktionen (und auch Eigenschaften) erzeugen, deren Name ein Symbol ist, lassen sich nur so erzeugen.
- Konkret ist die Namensquelle hier
Symbol.species
, das@@species
-Symbol. - Bei der so definierten
@@species
-Funktion handelt es sich nicht um eine Methode, sondern um den Getter einer Eigenschaft - Die @@species-Getter wird nicht für die Objekte definiert, die von
SpecialArray2
erzeugt werden, sondern ist statisch. Er ist also ein Eigenschafts-Getter der Konstruktorfunktion selbst.
Durch this.constructor[Symbol.species]
erhält man also das Ergebnis des @@species
-Getters, der auf der Konstruktorfunktion definiert ist oder den sie von einer Elternklasse erbt.
Wenn die filter()
-Methode also new (this.constructor[Symbol.species])()
verwendet, nutzt sie das @@species
-Ergebnis als Konstruktor. In unserem SpecialArray2
-Beispiel wird so bewirkt, dass die Filterung eines SpecialArray2
einfache Arrays zurückgibt.
Fazit
@@species
erfordert, dass beim Umgang mit Arrays, beim Matchen von Strings oder bei Verwendung von Promises zwei Zwischenschritte erforderlich sind, bevor das Ergebnisobjekt erzeugt werden kann. Zuerst muss über this.constructor
die richtige Konstruktorfunktion bestimmt werden. Und als zweites hat diese Konstruktorfunktion über den @@species
-Getter die Option, eine andere Konstruktorfunktion als ihren Vertreter zu benennen.
Eine Optimierung dieses Codes wird dadurch stark erschwert, weshalb das @@species-Protokoll keine gesicherte Zukunft hat.
Weblinks
ECMAScript-Spezifikation: get Array[@@species], get Map[@@species], get ArrayBuffer[@@species], get ArrayBuffer[@@species]
Referenzen
- ↑ TC39: Restrict subclassing support in built-in methods, abgerufen am 01.04.2023