JavaScript/Objekte/Map

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Mit dem eingebauten Konstruktor Map können Maps erstellt werden. Das sind keine Landkarten, sondern geordnete Listen, deren Einträge aus einem Schlüssel und einem Wert bestehen, wobei sowohl die Werte als auch die Schlüssel von einem beliebigen Datentyp sein können.

Eigenschaften von Map

Methoden von Map

Eigenschaften von Map.prototype

Methoden von Map.prototype

Erzeugen von Maps

Eine Map, in anderen Programmiersprachen auch als Dictionary oder assoziatives Array bekannt, wird durch den Aufruf des Konstruktors Map erzeugt.

Syntax

  map = new Map( [iterable] )

Wenn Sie die neue Map sofort mit Werten füllen möchten, können Sie das mit einem Array tun, das dafür einen passenden Aufbau haben muss. Jeder Eintrag darin muss wiederum ein Array sein, an dessen Indexposition 0 der Schlüssel für den neuen Eintrag erwartet wird und an Indexposition 1 der Wert.

Erzeugen einer Map mit Quadratzahlen
const squares = new Map([
    [1, 1],
    [2, 4],
    [3, 9],
    [4, 16],
    [5, 25],
    [6, 36],
    [7, 49]
]);

console.log('Das Quadrat von 3 ist ', squares.get(3)); // 9

Zugegeben, dieses Beispiel wäre mit einem einfachen Array statt einer Map einfacher umsetzbar gewesen. Wir schauen uns im Verlauf dieses Artikels noch andere Einsatzzwecke an.

Außer einem solchen Array können Sie zur Initialisierung jedes iterierbares Objekt verwenden, dessen Iterator diese [ schlüssel, wert ]-Arrays liefert. Ein Map-Objekt erfüllt diese Voraussetzung, Sie können also eine Map mit dem Inhalt einer anderen Map initialisieren.

Um einen Wert aus einer Map auszulesen, verwenden Sie die Methode get und übergeben ihr den gewünschten Schlüssel.

Beachten Sie:
  • Der Aufruf von Map() ohne das Schlüsselwort new ist nicht zulässig und bewirkt, dass JavaScript einen TypeError wirft.
  • Wenn das übergebene Argument nicht iterierbar ist, wirft JavaScript ebenfalls einen TypeError.

Ein Aufruf von new Map(), also ohne Argument, ist zulässig und erzeugt eine leere Map.

Beispiel
const capitals = new Map();
console.log(capitals.size);    // 0 - keine Einträge

Maps mit ungewöhnlichen Werten

Eine Map kann sowohl als Schlüssel wie auch als Wert jeden JavaScript-Typ haben, auch Objekte. Da auch Funktionen Objekte sind, lässt sich eine Map erstellen, die zu einer Funktion ihre erste Ableitung speichert. Im folgenden Beispiel werden Pfeilfunktionen verwendet.

Map mit Funktionen als Schlüssel und Wert
const f = x => x ** 2 + x,
      g = x => 3 * x ** 2 + 5;

const ableitung = new Map([
    [f, x => 2 * x + 1],
    [g, x => 6 * x]
]);

console.log('Der Wert der ersten Ableitung von f an der Stelle 3 lautet: ', ableitung.get(f)(3));

Der Aufruf ableitung.get(f) liest den Mapinhalt, der dem Funktionsobjekt in f zugeordet ist - die zuvor gespeicherte Ableitungsfunktion. Diese wird dann mit dem Argument 3 aufgerufen.

Umgang mit Maps

Sehen wir uns nun die übrigen Funktionen an, die das Map-Objekt bereitstellt und was bei der Verwendung zu beachten ist.

Lesen und Prüfen von Werten

Die bereits gezeigte get()-Methode erwartet als Argument einen beliebigen Wert und greift mit diesem Wert als Schlüssel auf die Map zu. Existiert ein solcher Schlüssel, wird der zugeordnete Wert zurückgegeben. Existiert der Schlüssel nicht, gibt get() den Wert undefined zurück. Wenn Sie das Argument weglassen, sucht get() nach dem Wert für den Schlüssel undefined.

Nun braucht man aber nicht immer den Wert. Zuweilen reicht es auch, zu wissen, ob ein bestimmter Schlüssel vorliegt. Und dann weist get() eine Ungenauigkeit auf: Es ist zulässig, unter einem Schlüssel den Wert undefined zu speichern. Wenn get() also undefined zurückgibt, ist nicht klar, ob der Schlüssel nicht vorhanden ist, oder ob darunter undefined gespeichert wurde. Deswegen gibt es die Methode has(), die true zurückgibt, wenn der übergebene Wert als Schlüssel verwendet wird, und sonst false:

has(schlüssel'))
const medics = new Map([
  ['McCoy' , { name: 'McCoy', vorname: 'Leonard', titel: 'Dr.' } ],
  ['Crusher', { name: 'Crusher', vorname: 'Beverly', titel: 'Dr.' } ]
]);

console.log(medics.has('McCoy'));        // true
console.log(medics.has('Bashir'));       // false

Schreiben von Werten

Um in der Map Werte zu speichern, können Sie nicht nur den Konstruktor verwenden, eine Map kann nachträglich erweitert und auch geändert werden. Dazu dient die Methode set, mit der Sie für einen einzelnen Schlüssel einen Wert zuweisen können. Der Rückgabewert von set() ist das Map-Objekt, auf dem set() aufgerufen wurde.

Hinzufügen eines Eintrags mit set(schlüssel, wert)
const capitals = new Map( [
   [ 'Frankreich', 'Paris' ],
   [ 'Deutschland', 'Bonn' ],
   [ 'Italien', 'Rom' ] 
] );
console.log('Die Hauptstadt von Deutschland ist: ', capitals.get('Deutschland'));   // Hm.
console.log('Die Hauptstadt von Belgien ist: ', capitals.get('Belgien'));           // Ups?

capitals.set('Belgien', 'Brüssel');
console.log('Die Hauptstadt von Belgien ist: ', capitals.get('Belgien'));   // Brüssel

Die capitals-Map wurde zunächst ohne Belgien initialisiert, weswegen der erste Versuch, mit get den Wert für 'Belgien' zu lesen, den Wert undefined liefert. Nachdem der Eintrag mit set hinzugefügt wurde, erhalten wir 'Brüssel'.

Mit set können Sie nicht nur Schlüssel hinzufügen, sondern auch einem existierenden Schlüssel einen neuen Wert zuweisen. Damit können wir die veraltete Information für Deutschland korrigieren:

Ändern einer vorhandenen Schlüssels mit set()
capitals.set('Deutschland', 'Berlin');
console.log('Die Hauptstadt von Deutschland ist: ', capitals.get('Deutschland'));   // Besser :)

Der Umstand, dass die set()-Methode das Map-Objekt zurückgibt, auf dem sie aufgerufen wurde, ermöglicht das Setzen von mehr als einem Schlüssel durch eine Verkettung von Aufrufen. Der Nutzen ist nicht sonderlich hoch, man spart damit bestenfalls ein paar Nanosekunden Laufzeit, aber Sie sparen beim Programmieren ein paar Sekunden Lebenszeit. Die Idee besteht darin, dass es dem Zugriffs-Operator . ziemlich egal ist, ob links von ihm eine einfache Variable steht, die das Objekt enthält, oder ein Ausdruck, der ein Objekt liefert.

Direkte Verkettung von set-Aufrufen
const shipClasses = new Map()
      .set('Constitution', [ 'Enterprise', 'Hood', 'Potemkin' ])
      .set('Galaxy', [ 'Enterprise-D', 'Yamato', 'Odyssey' ])
      .set('Nebula', [ 'Endeavour', 'Phoenix', 'Sutherland' ]);

Diese Map verwendet als Schlüssel Zeichenketten (den Namen der Schiffsklasse) und als Wert Arrays mit den Namen der Schiffe, die dieser Klasse angehören. Die set()-Aufrufe wurden für bessere Lesbarkeit auf mehrere Zeilen verteilt, man hätte auch alles hintereinander auf eine Zeile schreiben können. Sie müssen nur aufpassen, dass das Semikolon lediglich am Ende des gesamten Ausdrucks gesetzt wird.

Abgrenzung Array - Objekt - Map

Nach diesen ersten Beispielen soll nun die Frage geklärt werden, wo sich eine Map von anderen Strukturen wie einem Array oder einem Objekt unterscheidet. Schließlich kann man das einleitende Quadratzahlenbeispiel auch mit einem Array lösen, und im Artikel über Objekte steht, dass sich Objekte als assoziative Arrays verwenden lassen.

Für ein Array beantwortet sich die Frage leicht: Für Arrays sind nur Integerzahlen als Schlüssel zulässig. Und die length-Eigenschaft enthält nicht die Anzahl der Array-Einträge, sondern den Wert des höchsten verwendeten Index, plus 1.

Bei Objekten ist es so, dass man ein leeres Objekt durchaus als assoziatives Array verwenden kann. Die Reihenfolge, in der Methoden wie getOwnPropertyNames die Objekteigenschaften zurückliefern, Es gibt aber Einschränkungen:

  • Die Schlüssel dürfen nur Strings oder Symbole sein
  • Man muss darauf achten, das Objekt ohne Prototyp anzulegen (Object.create(null)), andernfalls könnten die von Object.prototype geerbten Methoden zu Konflikten führen
  • Es gibt keine size-Eigenschaft

Zumeist sind diese Einschränkungen nicht von Bedeutung. Aber dennoch: Der Sinn des Map-Objekts ist es, einen klar definierten Datentyp für die Speicherung von Schlüssel-Wert Paaren zu haben, sowie die gespeicherten Daten von den Eigenschaften und Methoden des Objekts zu trennen.

Ermitteln der Größe und der vorhandenen Schlüssel

Um sich über den Inhalt einer Map zu informieren, stellt Map.prototype die Eigenschaft size und die Methode keys zur Verfügung.

Größe und Schlüsselliste einer Map
const capitals = new Map( [
   [ 'Frankreich', 'Paris' ],
   [ 'Deutschland', 'Bonn' ],
   [ 'Italien', 'Rom' ] 
]);
console.log('Ich kenne ', capitals.size, ' Hauptstädte');
console.log('Sie heißen: ', Array.from(capitals.keys()));

Bei size ist zu beachten, dass dies zwar eine von Map.prototype geerbte Eigenschaft ist, der Inhalt sich aber stets auf die aktuelle Anzahl von Schlüsseln in der Map bezieht. Wenn Sie wissen möchten, wie man so etwas macht, lesen Sie den Abschnitt über Accessor-Deskriptoren im Artikel zu Property-Deskriptoren.

Die Methode keys() liefert nicht einfach ein Array mit Schlüsseln, sondern einen Iterator, den Sie durchlaufen müssen, um die Schlüssel zu erhalten. Das könnte man mit einer for...of-Schleife tun, für eine simple Ausgabe verwenden wir aber einfach Array.from(), um den Iterator zu lesen und die gelieferten Elemente in einem Array zu speichern.

Eine kompaktere Schreibweise wäre der Spread-Operator .... Und da console.log eine variadische Funktion ist, können wir auch ganz auf das Array verzichten, der Spread-Operator verteilt den Iteratorinhalt einfach auf so viele Parameter wie erforderlich, und die log-Methode gibt sie alle aus.

keys() mit Spread-Operator lesbar machen
console.log('Sie heißen: ', [... capitals.keys()] );
console.log('Sie heißen: ', ...capitals.keys() );    // Spread-Operator in variadischer Funktion

Eindeutigkeit der Schlüssel

Beachten Sie, dass ein Schlüssel immer nur einen Wert haben kann (ein Array aus Schiffsnamen ist auch nur ein Wert). Ein set()-Aufruf überschreibt den vorhandenen Wert, wenn der angegebene Schlüssel bereits existiert. Das gleiche passiert, wenn Sie dem Map() Konstruktor den gleichen Schlüssel mehrfach übergeben. Der letzte Eintrag zu einem Schlüsselwert gewinnt.

Der gleiche Schlüssel wird mehrfach verwendet
const deltaPeoples = new Map( [
      [ 'Borg', 'Cubes' ],
      [ 'Hirogen', undefined ],
      [ 'Talaxians', 'Talax' ],
      [ 'Borg', 'Unicomplex' ]
]);
console.log('Völker des Delta-Quadranten: ', ...deltaPeoples.keys());   // Borg, Hirogen, Talaxians
console.log('Heimat der Borg: ', deltaPeoples.get('Borg'));

Die Borg sind an erster Stelle geblieben. Aber sie stecken nun nicht mehr in ihren Cubes, sondern im Unicomplex.

Konstante Reihenfolge der Schlüssel

Die Schlüssel, die in der Map abgelegt werden, bilden eine Liste, die nach dem Zeitpunkt des Hinzufügens sortiert ist, d. h. neue Schlüssel werden immer hinten an der Liste angefügt. Das schauen wir uns noch einmal an Hand des Hauptstädte-Beispiels an. Zum Ausgeben der Schlüssel setzen wir den vorhin beschriebenen Spread-Operator ein.

Schlüsselreihenfolge in der Map
const capitals = new Map( [
    [ 'Frankreich', 'Paris' ],
    [ 'Deutschland', 'Bonn' ],
    [ 'Italien', 'Rom' ] 
] );
console.log('Ich kenne Hauptstädte für ', ...capitals.keys());

capitals.set('Belgien', 'Brüssel');
capitals.set('Deutschland', 'Berlin');
console.log('Jetzt kenne ich Hauptstädte für ', ...capitals.keys());

Die Reihenfolge der Einträge ist zunächst 'Frankreich', 'Deutschland' und 'Italien', also nicht alphabetisch, sondern genau die Reihenfolge, in der die Map initialisiert wurde. Nach der Ergänzung für Belgien und der Korrektur für Deutschland wurde 'Belgien' hinten angefügt, während 'Deutschland' an seiner Position geblieben ist.

Durchlaufen (Iterieren) einer Map

Eine Map ist ein iterierbares Objekt, d.h. Sie erhalten durch Aufruf der Symbol.iterator-Methode einen Iterator für den Inhalt der Map. Genaueres Hinschauen zeigt, dass diese Methode identisch ist mit der Methode entries von Map.prototype. Einen Iterator im „Handbetrieb“ zu durchlaufen ist allerdings mühsam (lesen Sie dazu den Artikel über Iteratoren), mit der for...of-Schleife ist es deutlich bequemer.

Wenn Sie diesen Iterator durchlaufen, erhalten Sie pro Schritt ein Array mit zwei Elementen. Dabei handelt es sich um einen Schlüssel und den zugeordneten Wert. Deshalb ist eine Map dazu geeignet, als Argument für new Map() verwendet zu werden - der Iterator erzeugt genau die Werte, die der Konstruktor erwartet.

Iterieren einer Map
const crew = new Map( [
   ['Picard', 'Jean-Luc'],
   ['Riker', 'William'],
   ['LaForge', 'Geordi'],
   ['Crusher', 'Wesley']
] );

for (const member of crew) {
   console.log('I got', member[1], ' ', member[0]);
]);
// I got Jean-Luc Picard
// I got William Riker
// I got Geordi LaForge
// I got Wesley Crusher

Eine weitere Möglichkeit, die Einträge einer Map zu durchlaufen, ist die Methode forEach. Wie bei Array werden die Einträge der Map nacheinander an eine Callback-Funktion übergeben. Dabei ist der erste Parameter der Callback-Funktion der Wert des gerade bearbeiteten Eintrags, der zweite Parameter ist der zugehörige Schlüssel und der dritte Parameter ist die Map, die durchlaufen wird. Der Aufruf des forEach-Callbacks von Map ist also vergleichbar mit dem von Array, nur ist der Arrayindex durch den Mapschlüssel ersetzt.

Durchlaufen einer Map mit forEach
// crew-Map aus dem vorigen Beispiel

crew.forEach((value, key, map) => {
   console.log('I got ', value, ' ', key);
});
// I got Jean-Luc Picard
// I got William Riker
// I got Geordi LaForge
// I got Wesley Crusher

Genau wie bei der forEach()-Methode von Arrays können Sie außer dem Callback noch ein weiteres Argument übergeben, das beim Aufruf des Callbacks als Kontextobjekt verwendet wird, also in this verfügbar ist.

Wenn Sie nur die Werte auflisten möchten und sich für die Schlüssel dazu nicht interessieren, können Sie die values-Methode verwenden. Die Werte werden in der Reihenfolge iteriert, wie die (Schlüssel,Wert)-Paare in der Map stehen.

Schlüsselreihenfolge in der Map
const capitals = new Map( [
    [ 'Frankreich', 'Paris' ],
    [ 'Deutschland', 'Bonn' ],
    [ 'Italien', 'Rom' ] 
] );
console.log('Ich diese Hauptstädte: ', ...capitals.values());
// oder so:
for (const capital of capitals) {
   console.log('Ich kenne die Hauptstadt ', capital);
}
Beachten Sie: Sie können die forEach()-Methode auf die Map anwenden, aber nicht auf die Iteratoren, die von keys(), entries() oder values() zurückgegeben werden.

Manipulieren einer Map während der Iteration

Dringender Rat: Lassen Sie das sein. Manipulieren Sie eine Liste niemals, während sie durchlaufen wird. Auf diesem Weg liegen Kopfschmerzen und lange Debug-Sitzungen.

Aber leider, bei Maps geht es. Das Iterator-Objekt für Maps kommt damit zurecht, wenn Sie während einer Iteration einen Eintrag aus der Map entfernen oder einen neuen Eintrag hinzufügen. Neue Elemente werden ohnehin am Ende der Schlüsselliste angefügt, so dass die Iteration sie unweigerlich erreicht. Aber der Iterator kommt auch zurecht, wenn Sie ein bereits durchlaufenes Element, das gerade besuchte Element oder eins der noch zu durchlaufenen Elemente entfernen.

Beispiel
const assimilated = new Map( [ ['Picard', 'Jean-Luc'], [ 'Hansen', 'Annika' ] ] );

assimilated.forEach(function (value, key, map) {
  console.log('assimiliere', value, key);
  switch (key) {
    case 'Picard' :
      map.delete(key);
      map.set('of Borg', 'Locutus');
      break;
    case 'Hansen' :
      map.delete(key);
      map.set('of Nine', 'Seven');
      break;
  }
});

Die Schleife assimiliert Captain Picard und Annika Hansen, indem sie die bisherigen Einträge löscht und durch Locutus of Borg und Seven of Nine ersetzt. Da dies neue Schlüssel sind, werden sie ans Ende der Schlüsselliste gesetzt, weshalb die forEach-Methode sie im Anschluss an Picard und Hansen antrifft und verarbeitet. Die Schleife fragt ausdrücklich auf bestimmte Namen ab. Würde sie blindlings jeden zu assimilieren versuchen (zum Beispiel so, dass ein "#" an den Key angehängt wird), geriete das Programm in eine Endlosschleife. Erwähnten wir schon, dass das Manipulieren einer Liste während des Durchlaufens keine gute Idee ist?

Entfernen von Einträgen

Um einen einzelnen Eintrag aus einer Map zu entfernen, wird die delete-Methode verwendet. Sie erwartet einen Schlüsselwert als Argument und entfernt den zugehörigen Eintrag aus der Map. Als Rückgabewert erhalten Sie true, wenn es diesen Schlüssel gab und false, wenn Sie einen nicht existierenden Schlüssel löschen wollten.

Beispiel
const awayTeam = new Map([
  ['Riker', 'William'],
  ['Crusher', 'Beverly'],
  ['Data', 'Android'],
  ['Yar', 'Natasha']
]);

function Armus(intruders) {
   intruders.delete('Yar');
}

console.log(Armus(awayTeam));         // true, leider

Wenn Sie nicht wissen, welche Geschichte dieses Codebeispiel erzählt, schauen Sie sich die Episode „Die schwarze Seele“ (Skin of Evil) aus Star Trek Next Generation, Staffel 1, an.

Um eine Map vollständig zu leeren, muss man nicht jeden einzelnen Schlüssel löschen, dafür gibt es die Methode clear:

Beispiel
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

Zugegeben, das Beispiel ist nicht ganz korrekt. Bei Wolf 359 waren 40 Schiffe im Einsatz, und eins blieb übrig...

Spezifikation

Weblinks