JavaScript/Objekte/Map
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.prototype
- @@toStringTag (enthält
"Map"
) - constructor
- size
Methoden von Map.prototype
Inhaltsverzeichnis
- 1 Erzeugen von Maps
- 2 Umgang mit Maps
- 2.1 Lesen und Prüfen von Werten
- 2.2 Schreiben von Werten
- 2.3 Abgrenzung Array - Objekt - Map
- 2.4 Ermitteln der Größe und der vorhandenen Schlüssel
- 2.5 Eindeutigkeit der Schlüssel
- 2.6 Konstante Reihenfolge der Schlüssel
- 2.7 Durchlaufen (Iterieren) einer Map
- 2.8 Manipulieren einer Map während der Iteration
- 2.9 Entfernen von Einträgen
- 3 Spezifikation
- 4 Weblinks
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.
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.
- Der Aufruf von
Map()
ohne das Schlüsselwortnew
ist nicht zulässig und bewirkt, dass JavaScript einenTypeError
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.
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.
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.
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:
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.
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.
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.
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.
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.
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.
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.
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.
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);
}
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.
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.
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:
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
- ECMAScript: The Map Constructor
Weblinks
- SELF-Forum: Assoziative Arrays (Umgang mit Maps) von Orlok 10.06.2016
- MDN: Map