Orlok: Assoziative Arrays

Beitrag lesen

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.

Maps

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

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

const assimilated = new Map( )
.set('Jean-Luc', 'Picard')
.set('Annika', 'Hansen');

assimilated.forEach(function (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