Orlok: Assoziative Arrays

Beitrag lesen

Hallo dedlfix

In Javascript gibt es ja keine assoziativen Arrays.

Seit ECMAScript 2015 gibt es eine native Implementierung dieses Konzepts mit dem Namen Map, bei der Instanzen durch den Aufruf des gleichnamigen Konstruktors erzeugt werden. Bei einer Map können sowohl die Werte als auch die Schlüssel von einem beliebigen Datentyp sein, da die Einträge, anders als bei Arrays oder planen Objekten, nicht als Objekteigenschaften angelegt werden.

Mit anderen Worten, man hat mit viel Aufwand etwas implementiert, was gefühlt 99,9% der Anwender bereits mit herkömmlichen Javascript-Objekten realisieren können und das zu einem ebenso hoch vermuteten Anteil auch weiterhin so realisiert werden wird, weil für diese Anwendungsfälle Strings als Keys reichen.

Das ist ziemlicher Unsinn und das wüsstest du selbst, wenn du dir die Mühe gemacht hättest, mehr als bloß den ersten Absatz meines Beitrags zu lesen und ein wenig darüber nachzudenken. Schließlich gibt es eine Menge Unterschiede zwischen Maps und gewöhnlichen Objekten und es ist keinesfalls so, dass die Möglichkeit, Schlüssel mit anderen Datentypen als String und Symbol zu verwenden, der einzige Vorteil wäre, den Maps gegenüber planen Objekten bieten würden.

Die Frage, wann eine Map und wann ein gewöhnliches Objekt zu verwenden ist, lässt sich nicht pauschal beantworten, sondern die Antwort hängt von den Voraussetzungen des konkreten Anwendungsfalls ab, wobei der Datentyp der Schlüssel nur ein Aspekt ist, den es hierbei zu berücksichtigen gilt. Andere Aspekte wären zum Beispiel Performanz oder die Fragen, ob der Datensatz zur Laufzeit manipuliert werden soll oder nicht, ob gegebenenfalls nur auf einzelnen Einträgen operiert werden soll oder ob entsprechende Operationen mehrere oder gar alle Einträge betreffen und falls letzteres, ob die Reihenfolge der Einträge dabei eine Rolle spielt.

Nur weil assoziative Arrays bislang in Ermangelung einer nativen Implementierung in der Regel über plane Objekte realisiert wurden, bedeutet das nicht, dass eine solche Lösung unproblematisch wäre, das heißt, die Verwendung gewöhnlicher Objekte zu diesem Zweck war eigentlich schon immer eine Notlösung, die mit einigen Makeln behaftet ist. Zwar lassen sich die meisten Klippen umschiffen, aber eben nicht alle, und entsprechende Maßnahmen sind zwangsläufig entweder mit einer Einschränkung der Nutzbarkeit oder einer Erhöhung der Komplexität verbunden. Das ist der Grund, weshalb Maps eingeführt wurden.

So ist bei der Verwendung von gewöhnlichen Objekten zum Beispiel zu berücksichtigen, dass diese standardmäßig Eigenschaften und Methoden über die Prototypenkette erben. Es besteht also grundsätzlich das Risiko, dass der Datensatz auf diese Weise kontaminiert wird. Natürlich sind die Eigenschaften und Methoden, die von Object.prototype vererbt werden, nicht abzählbar und es ist auch unwahrscheinlich, dass in einem Datensatz Schlüssel verwendet werden, die mit deren Namen identisch sind, aber es ist in vielen Fällen dennoch geboten, diesem Umstand Rechnung zu tragen.

// dictionary pattern

const object = Object.create(null);

Will man das Risiko also ausschließen, dass der Datensatz durch geerbte Eigenschaften kontaminiert wird, dann bleiben einem nur zwei Handlungsweisen, nämlich entweder bei jeder Operation auf den Daten zunächst zu prüfen, ob es sich um eine eigene Eigenschaft des jeweiligen Objektes handelt, was mit der Methode hasOwnProperty bewerkstelligt werden kann, oder aber bei der Erzeugung des Objektes explizit den Wert null als Prototyp zu bestimmen, was unter Verwendung der Methode create umzusetzen wäre. Letzteres hat jedoch den Nachteil, dass die komfortable Notierung der Einträge in einem Objektliteral ausscheidet und die Befüllung des Objektes mit Einträgen auf andere Weise erfolgen muss.

Wird hingegen eine Map verwendet, dann braucht man sich über dieses Thema grundsätzlich keine Gedanken zu machen, da die Einträge hier wie gesehen nicht als Objekteigenschaften angelegt werden. Abhängig von den Umständen des konkreten Anwendungsfalls, kann dies also durchaus ein gewichtiges Argument für den Einsatz von Maps darstellen.

Ein weiterer Unterschied zwischen Maps und gewöhnlichen Objekten besteht darin, dass es für plane Objekte keine eingebaute Eigenschaft oder Methode gibt, um auf direktem Wege die Anzahl der Einträge, also der eigenen Eigenschaften zu ermitteln. Zwar existieren verschiedene Möglichkeiten, um an diese Information zu gelangen, aber die entsprechende Funktionalität muss eben eigenhändig implementiert werden, wohingegen es bei Maps genügt, den Wert der Eigenschaft size zu lesen.

Darüber hinaus ist zu berücksichtigen, dass bei planen Objekten nicht garantiert werden kann, dass die Eigenschaften in derselben Reihenfolge ausgegeben werden, in der sie angelegt wurden. Man kann sich also nicht darauf verlassen, dass beispielsweise von einer for in Schleife oder einer der Methoden zur Examinierung der Schlüssel eines Objektes, die ursprüngliche Reihenfolge eingehalten wird.

Das ist bei Maps grundsätzlich anders, denn wie ich in meiner ersten Antwort bereits schrieb, werden die Einträge hier prinzipiell in der Reihenfolge in der internen Liste gespeichert, in welcher sie der Map hinzugefügt wurden, und bei einer Ausgabe, entweder beim Kopieren in ein Array oder eine andere Datenstruktur, oder bei der Iteration mit einer for of Schleife, bleibt die Reihenfolge erhalten.

Ist die Reihenfolge der Einträge eines entsprechenden Datensatzes also von Belang, dann ist dies ein sehr starkes Argument, das für dein Einsatz einer Map anstelle eines planen Objektes spricht.

Aber auch unabhängig von diesem Aspekt, gestaltet sich die Arbeit mit Maps wesentlich einfacher, wenn Operationen auf dem ganzen Datensatz durchgeführt werden sollen und entsprechend über die Einträge zu iterieren ist. Anders als bei gewöhnlichen Objekten, gibt es wie bereits erwähnt für Maps nämlich eine eingebaute Methode forEach, sodass hier eine Funktionalität bereitgestellt wird, die sonst nur für Arrays und Sets zur Verfügung steht, nicht jedoch für plane Objekte. Das heißt, auch in diesem Fall kann natürlich dasselbe Ziel erreicht werden, wenn man ein gewöhnliches Objekt verwendet, aber die Umsetztung ist weit weniger komfortabel.

Dies gilt allerdings generell für den Fall, dass Operationen nicht nur auf einzelnen Einträgen durchgeführt werden sollen. So ist auch zu berücksichtigen, dass es sich bei Maps, ebenso wie bei Arrays und Sets um iterierbare Objekte handelt, wohingegen gewöhnliche Objekte standardmäßig nicht iterierbar sind. Das heißt, es ist ein Kinderspiel, etwa unter Verwendung des Spreadoperators die Einträge in ein Array zu kopieren, darauf zu operieren und mit dem Ergebnis dann wieder eine Map zu befüllen, während dieselben Aktionen bei einem gewöhnlichen Objekt mit ungleich größerem Aufwand verbunden wären.

Schließlich scheinen Maps bereits jetzt bei den meisten Ausführungsumgebungen die sie unterstützen performanter zu sein als es plane Objekte sind und es ist zu erwarten, dass dieser Vorsprung in Zukunft eher noch wachsen wird, zumal in Bezug auf Maps vermutlich noch ein größeres Potential für Optimierungen besteht, als dies bei gewöhnlichen Objekten der Fall ist.

Zusammenfassend lässt sich also festhalten, dass sich pauschale Aussagen darüber verbieten, welcher der beiden Techniken der Vorzug zu geben ist, da dies immer von den jeweiligen Anforderungen im Einzelfall abhängt. Als Faustregel lässt sich jedoch sagen, dass gewöhnlichen Objekten immer dann der Vorzug zu geben ist, wenn es sich nur um vergleichsweise wenige Daten handelt, die hinterlegt werden sollen und diese nach der Initialisierung auch nicht mehr groß verändert werden. Sind jedoch größere Operationen auf den Daten notwendig, dann spricht viel dafür, statt planer Objekte Maps zu verwenden, da sich die Arbeit mit diesen speziellen Objekten in vielen Punkten wesentlich einfacher gestaltet.

Gruß,

Orlok