JavaScript/Objekte/Symbol/species

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

In Symbol.species finden Sie das well-known Symbol @@species.


Syntax

Symbol.species


Attribute
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!

Die im folgenden beschriebenen flexiblen Möglichkeiten, die @@species bietet, führen zu einer hohen Komplexität. Sie behindern die Optimierung des Bytecodes, in den JavaScript für die Ausführung umgewandelt wird, und können zu schwer nachvollziehbaren Programmfehlern führen. Es gibt daher seitens Mozilla und Google eine Initiative[1], @@species als Designfehler festzustellen und aus der Sprache wieder zu entfernen.

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.

Subklassen von Arrays
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 die SpecialArray-Funktion die Eigenschaft @@species, die auf Array definiert ist.
  • Die @@species-Eigenschaft ist mit einer Getter-Funktion realisiert. Ruft man SpecialArray[Symbol.species] auf, wird diese Getter-Funktion aufgerufen und gibt this zurück - und das ist die SpecialArray-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. Wenn specArr ein SpecialArray-Objekt ist, dann liefert specArr.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:

Subklassen von Arrays mit abweichender @@species
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:

  1. Es wird eine Funktion definiert
  2. Die Funktion gibt einen Verweis auf die Array-Konstruktorfunktion zurück
  3. 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.
  4. Konkret ist die Namensquelle hier Symbol.species, das @@species-Symbol.
  5. Bei der so definierten @@species-Funktion handelt es sich nicht um eine Methode, sondern um den Getter einer Eigenschaft
  6. 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

  1. TC39: Restrict subclassing support in built-in methods, abgerufen am 01.04.2023