Assoziative Arrays
bearbeitet von
Hallo dedlfix
> In Javascript gibt es ja keine assoziativen Arrays.
Dem wage ich zu widersprechen. ;-)
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.
{: style="outline:1px solid darkgrey;"}
Vielmehr ist es so, dass die Einträge einer Map in einer internen, mit der jeweiligen Map verknüpften Liste gespeichert werden und entsprechend erfolgt der Zugriff auf die hinterlegten Einträge hier auch nicht wie bei Objekteigenschaften über die Punkt- oder Klammernotation, sondern über bestimmte, für diesen Zweck vorgesehene Methoden.
---
Anders als bei einigen anderen Features dieser Edition des Standards, sieht es bei der Kompatibilität auch schon [relativ gut](http://kangax.github.io/compat-table/es6/#test-Map) aus, aber es ist dennoch anzuraten, bei der gegenwärtigen Verwendung von Maps ein Polyfill bereitzustellen. Da zu vermuten ist, dass Ausführungsumgebungen, die diese Datenstruktur nicht unterstützten, auch keine Kompatibilität mit Sprachelementen bieten, die das *Iteration Protocol* implementieren, sollte jedoch auf den Gebrauch der entsprechenden Funktionalität im Zusammenhang mit Maps vorerst vermutlich besser verzichtet werden. Der Nachbau der Basisfunktionalität für ein Polyfill ist jedenfalls nicht übermäßig kompliziert, weshalb ich davon ausgehe, dass eine entsprechende Suche zu dem Thema einige Ergebnisse zu Tage bringen würde.
---
Sehen wir uns nun aber einmal etwas genauer an, wie mit Maps gearbeitet werden kann, welche Methoden von der Standardbibliothek bereitgestellt werden und was bei der Verwendung zu beachten ist.
~~~ javascript
const instance = new Map;
console.log(instance); // [object Map]
~~~
Hier wäre zunächst zu erwähnen, dass Maps durch den Aufruf des Konstruktors Map erzeugt werden, wobei aber zu beachten ist, dass Map auch wirklich als *Konstruktor* aufgerufen werden muss, also entweder über den Operator new oder über die Methode construct des Standardobjektes Reflect, welches jedoch ebenfalls erst seit der sechsten Edition der Sprache ein Teil des Standards ist. Wird der Konstruktor Map als gewöhnliche Funktion aufgerufen, dann wird hierdurch ein Typfehler produziert.
~~~ javascript
const crew = new Map([
['Picard', 'Jean-Luc'],
['Riker', 'William']
]);
~~~
Soll die Mapinstanz bei ihrer Erzeugung mit Einträgen initialisiert werden, dann können diese beim Aufruf des Konstruktors in einem iterierbaren Objekt übergeben werden, also etwa wie in dem Beispiel oben als Elemente eines Arrays. Dabei sind die einzelnen Einträge wiederum in Array-ähnlichen Objekten zu übergeben, deren erstes Element den Schlüssel spezifiziert und das zweite Element den Wert. Die Einträge werden hierbei prinzipiell in der selben Reihenfolge in die interne Liste der Map eingefügt, wie sie in der als Argument übergebenen Datenstruktur vorliegen, bei der es sich übrigens auch um eine andere Map handeln kann, da Maps ebenfalls iterierbare Objekte sind.
~~~ javascript
const classes = new Map( )
.set('Galaxy', [
'Enterprise',
'Yamato',
'Odyssey'
])
.set('Nebula', [
'Endeavour',
'Phoenix',
'Sutherland'
]);
~~~
Nach der Erzeugung der Map können Einträge nur noch einzeln hinzugefügt werden, durch den Aufruf der Mapmethode `set`, welche als erstes Argument den Schlüssel und als zweites Argument den Wert erwartet. Der Rückgabewert der Methode ist dabei grundsätzlich die Map auf der sie aufgerufen wurde, was recht praktisch ist, weil hierdurch wie in dem Beispiel oben mehrere Aufrufe verkettet werden können.
~~~ javascript
const engineers = new Map([
['Scott', 'Montgomery'],
['La Forge', 'Geordi']
]);
console.log(engineers.get('Scott')); // Montgomery
~~~
Das Pendant zu set für den lesenden Zugriff auf einen Eintrag ist die Methode `get`, die als Argument den Schlüssel erwartet und falls vorhanden den dazugehörigen Wert zurückgibt. Gibt es keinen Eintrag für den angegebenen Schlüssel, dann wird der Wert undefined zurückgegeben.
~~~ javascript
const medics = new Map([
['McCoy', 'Leonard'],
['Crusher', 'Beverly']
]);
console.log(medics.has('McCoy')); // true
~~~
Ob in einer Map ein bestimmter Eintrag existiert, kann mit der Methode `has` ermittelt werden, welcher der jeweilige Schlüssel als Argument übergeben wird und die als Ergebnis der Prüfung einen booleschen Wert zurückgibt.
~~~ javascript
const androids = new Map([
['Dr.Soong', 'Lore']
['Dr.Soong', 'Data']
]);
console.log(androids.get('Dr.Soong')); // Data
~~~
Es ist allerdings grundsätzlich zu beachten, dass alle in einer Map verwendeten Schlüssel individuell sein müssen. Wenn bei der Initialisierung der Map oder beim späteren Hinzufügen eines Eintrags durch die Methode set ein Schlüssel angegeben wird, zu dem in der Map bereits ein Eintrag existiert, dann wird der alte Eintrag durch den neuen Eintrag überschrieben.
~~~ javascript
const states = new Map( )
.set('Federation', [
'Humans',
'Vulcans',
'Bolians'
])
.set('Dominion', [
'Founders',
'Jem’Hadar',
'Vorta'
]);
console.log(states.size); // 2
~~~
Darüber hinaus gibt es bei Maps – ähnlich der Eigenschaft length bei Arrays – eine Eigenschaft mit dem Namen `size`, welche die Anzahl der in einer Map enthaltenen Einträge zurückgibt. Dabei handelt es sich jedoch nicht um eine eigene Eigenschaft der Instanzen, sondern size ist als Getter auf `Map.prototype` definiert und wird lediglich im Kontext der jeweiligen Map aufgerufen. Da es keinen dazugehörigen Setter gibt, kann die Eigenschaft nicht gesetzt sondern nur gelesen werden.
~~~ javascript
const crew = new Map([
['Tasha', 'Yar']
]);
const Armus = member => crew.delete(member);
console.log(Armus('Tasha')); // true
~~~
Soll ein einzelner Eintrag aus einer Map entfernt werden, dann ist hierfür die Methode `delete` zu verwenden, welche den Schlüssel für den zu löschenden Eintrag als Argument erwartet. Abhängig davon, ob es in der Map einen Eintrag für den angegebenen Schlüssel gab der gelöscht werden konnte, wird entweder true oder false zurückgegeben.
~~~ javascript
const starfleet = new Map([
[57301, 'Chekov'],
[65491, 'Kyushu'],
[62043, 'Melbourne'],
[31911, 'Saratoga'],
[62095, 'Tolstoy']
]);
function wolf359 (federation, borg) {
if (borg) {
federation.clear( );
}
}
wolf359(starfleet, 'Cube');
console.log(starfleet.size); // 0
~~~
Wird die Methode `clear` auf einer Map aufgerufen, dann werden *alle* Einträge der Map auf einmal gelöscht. Der Rückgabewert von clear ist grundsätzlich der Wert undefined.
Damit hätten wir im Prinzip den Großteil der eingebauten Funktionalität abgehandelt. Was nun noch bleibt, ist das Thema Iteration, das ja auch von einigem Interesse ist. Hierbei ist anzumerken, dass für Maps standardmäßig drei verschiedene *Iterable Interfaces* bereitsgestellt werden, und zwar durch die Methoden `entries`, `keys` und `values`, wobei sich die hierbei ausgegebenen Werte aus den Namen der Methoden herleiten lassen. Das *Default Interface* wird bei Maps übrigens durch die Methode entries gestellt, das heißt, wenn etwa bei der Verwendung einer Schleife mit `for` und `of` nur eine Referenz auf eine Map übergeben wird, dann wird die Schleifenvariable mit den Einträgen der Map initialisiert, in Form von Arrays mit zwei Elementen.
~~~ javascript
const ships = new Map([
[2021, 'Farragut'],
[2893, 'Stargazer']
]);
for (let name of ships.values( )) {
console.log(name); // Farragut, Stargazer
}
~~~
Soll also mit einer for-of-Schleife beispielsweise *nur* über die Werte einer Map iteriert werden, dann ist die Methode values aufzurufen, welche ein Iteratorobjekt zurückgibt, dessen Methode `next` beim internen Aufruf durch die Schleife nur die *Werte* weiterreicht. Soll hingegen nur über die Schlüssel der Map iteriert werden, wäre entsprechend die Methode keys aufzurufen.
Die for-of-Schleife stellt aber natürlich nicht die einzige Möglichkeit dar, um über eine Map zu iterieren, denn darüber hinaus gibt es auch noch eine Mapmethode `forEach`, welche im Prinzip genauso funktioniert wie die gleichnamige Methode, die von Array.prototype vererbt wird.
~~~ javascript
map.forEach(function (value, key, map) { }, thisArg);
~~~
Die Methode forEach erwartet als erstes Argument die Rückruffunktion und als optionales zweites Argument den Wert, in dessen Kontext die Funktion aufgerufen werden soll, was für jeden Eintrag der Map einmal passiert, in der Reihenfolge in der die Einträge der Map hinzugefügt wurden. Dabei wird die Rückruffunktion von der Methode forEach mit drei Argumenten aufgerufen, nämlich dem *Wert* des Eintrags, dem *Schlüssel* und einer Referenz auf die *Map*, über die iteriert wird.
~~~ javascript
const assimilated = new Map( )
.set('Jean-Luc', 'Picard')
.set('Annika', 'Hansen');
assimilated.forEach((value, key, map) => {
switch (value) {
case 'Picard' :
map.delete(key);
map.set('Locutus', 'of Borg');
break;
case 'Hansen' :
map.delete(key);
map.set('Seven', 'of Nine');
break;
default : console.log(key); // Locutus, Seven
}
});
~~~
Wird von der an forEach übergebenen Rückruffunktion ein Eintrag gelöscht, bevor dieser erreicht wurde, dann wird die Funktion für diesen Eintrag nicht mehr aufgerufen, es sei denn, er wird vor Beendingung der Methodenausführung der Map wieder hinzugefügt. Bei der Iteration werden also grundsätzlich auch solche Einträge berücksichtigt, die während der Ausführung an die Map übergeben wurden.
---
Es bleibt also festzuhalten, dass es durchaus auch in **ECMAScript** eine native Implementierung von assoziativen Datenfeldern gibt, dass jedoch im Moment noch nicht alle für ein aktuelles Projekt relevanten Ausführungsumgebungen diesen Objekttyp unterstützen, weshalb Maps noch nicht ohne Netz und doppelten Boden verwendet werden können.
Viele Grüße,
Orlok