SELF-Treffen in Mannheim 2025

SELFHTML wird 30 Jahre alt! → Veranstaltungs-Ankündigung.

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 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.

Die filter()-Methode verwendet deshalb für ihre Rückgabe nicht einfach new Array(). Statt dessen nutzt sie aus, dass ein korrekt erstelltes Prototyp-Objekt die Eigenschaft constructor besitzt, die auf die Konstruktorfunktion verweist, mit der das Objekt erstellt wurde, das diesen Prototypen nutzt. Das heißt: sie könnte das Objekt, das sie zurückgeben will, mittels new this.constructor() erstellen und erhielte damit ein SpecialArray-Objekt. Das funktioniert. Solange, wie die gefundene Konstruktorfunktion verwendbar ist.

Das ist sie aber nicht immer. Beispielsweise kann man Array.prototype.filter.call(...) benutzen, um eine NodeList zu filtern. Ein NodeList-Objekt besitzt zwar eine Konstruktorfunktion, aber sie wirft einen TypeError, wenn man sie aufrufen will. NodeList-Objekte können nur intern im DOM erstellt werden.

Deshalb wurde ein anderes Protokoll „erfunden“, um die Aufgabe „erstelle mir ein Objekt wie dieses” lösen zu können: die Symbol.species-Eigenschaft (kurz: @@species) der Konstruktorfunktion. Der NodeList-Konstruktor besitzt diese Eigenschaft beispielsweise nicht, weshalb der Aufruf der filter()-Methode auf einer NodeList ein Array liefert und keine neue NodeList.

Dadurch, dass eine Konstruktorfunktion die @@species-Eigenschaft nicht besitzt, weiß filter(), dass es sie nicht verwenden darf und erstellt ein neues Array. Im Fall unseres SpecialArray-Beispiels ist es so, dass es Teil der extends-Definition ist, dem Subklassen-Konstruktor die passende @@species-Eigenschaft zuzuweisen, so dass die filter()-Methode mittens new this.constructor[Symbol.species]() ihr Ergebnisobjekt erstellen kann.

Probleme von @@species

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?

Aus diesem Grund wurde @@species nicht als Eigenschaft definiert, sondern als Property-Getter. Dieser Getter kann von der Subklasse überschrieben werden, so dass beliebige Species-Werte darstellbar sind. Da dieser Getter keinen normalen Namen hat, sondern über ein well-known Symbol erreicht wird, ist die Syntax 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), 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 das Ergebnis des @@species-Getters, der auf der Konstruktorfunktion definiert ist oder den sie von seiner 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.

Zum einen hat es sich gezeigt, dass auf diese Weise Anwendercode plötzlich an Stellen ausgeführt wird, wo man nicht damit gerechnet hat. Dadurch sind Sicherheitslücken und Browser-Crashes ausgelöst worden. Hinzu kommt, dass die Optimierung von Code durch @@species-Konstruktion stark erschwert wird. Die filter()-Methode für Arrays kann beispielsweise nicht mehr davon ausgehen, ihr Ergebnis in ein echtes Array zu schreiben, weshalb sie nun aus zwei Teilen besteht: (a) Ausgabe ist ein echtes Array und (b) die Ausgabe ist ein array-artiges Objekt. Im Fall (b) ist filter() signifikant langsamer.

Aus diesem Grund diskutieren die für ECMAScript verantwortlichen Gremien seit 2020 darüber, ob man @@species generell abschaffen kann ohne große Kompatibilitätsprobleme auszulösen. Die Entscheidung ist (März 2025) offen.

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