JavaScript für ungeduldige Programmierer (ES2022-Ausgabe)
Bitte unterstützen Sie dieses Buch: kaufen Sie es oder spenden Sie
(Werbung, bitte nicht blockieren.)

33 Maps (Map)



Vor ES6 gab es in JavaScript keine Datenstruktur für Wörterbücher, und Objekte wurden als Wörterbücher von Strings zu beliebigen Werten (miss-)braucht. ES6 brachte Maps, die Wörterbücher von beliebigen Werten zu beliebigen Werten sind.

33.1 Maps verwenden

Eine Instanz von Map ordnet Schlüssel Werten zu. Eine einzelne Schlüssel-Wert-Zuordnung wird als Eintrag bezeichnet.

33.1.1 Maps erstellen

Es gibt drei gängige Arten, Maps zu erstellen.

Erstens können Sie den Konstruktor ohne Parameter verwenden, um eine leere Map zu erstellen

const emptyMap = new Map();
assert.equal(emptyMap.size, 0);

Zweitens können Sie dem Konstruktor ein Iterable (z. B. ein Array) von Schlüssel-Wert-„Paaren“ (Arrays mit zwei Elementen) übergeben

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'], // trailing comma is ignored
]);

Drittens fügt die Methode .set() Einträge zu einer Map hinzu und ist verkettbar

const map = new Map()
  .set(1, 'one')
  .set(2, 'two')
  .set(3, 'three');

33.1.2 Maps kopieren

Wie wir später sehen werden, sind Maps auch Iterables über Schlüssel-Wert-Paare. Daher können Sie den Konstruktor verwenden, um eine Kopie einer Map zu erstellen. Diese Kopie ist flach: Schlüssel und Werte sind dieselben; sie werden nicht dupliziert.

const original = new Map()
  .set(false, 'no')
  .set(true, 'yes');

const copy = new Map(original);
assert.deepEqual(original, copy);

33.1.3 Mit einzelnen Einträgen arbeiten

.set() und .get() dienen zum Schreiben und Lesen von Werten (anhand von Schlüsseln).

const map = new Map();

map.set('foo', 123);

assert.equal(map.get('foo'), 123);
// Unknown key:
assert.equal(map.get('bar'), undefined);
// Use the default value '' if an entry is missing:
assert.equal(map.get('bar') ?? '', '');

.has() prüft, ob eine Map einen Eintrag mit einem bestimmten Schlüssel hat. .delete() entfernt Einträge.

const map = new Map([['foo', 123]]);

assert.equal(map.has('foo'), true);
assert.equal(map.delete('foo'), true)
assert.equal(map.has('foo'), false)

33.1.4 Größe einer Map bestimmen und sie leeren

.size enthält die Anzahl der Einträge in einer Map. .clear() entfernt alle Einträge einer Map.

const map = new Map()
  .set('foo', true)
  .set('bar', false)
;

assert.equal(map.size, 2)
map.clear();
assert.equal(map.size, 0)

33.1.5 Schlüssel und Werte einer Map erhalten

.keys() gibt ein Iterable über die Schlüssel einer Map zurück

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const key of map.keys()) {
  console.log(key);
}
// Output:
// false
// true

Wir verwenden Array.from(), um das von .keys() zurückgegebene Iterable in ein Array zu konvertieren

assert.deepEqual(
  Array.from(map.keys()),
  [false, true]);

.values() funktioniert wie .keys(), nur für Werte anstelle von Schlüsseln.

33.1.6 Einträge einer Map erhalten

.entries() gibt ein Iterable über die Einträge einer Map zurück

const map = new Map()
  .set(false, 'no')
  .set(true, 'yes')
;

for (const entry of map.entries()) {
  console.log(entry);
}
// Output:
// [false, 'no']
// [true, 'yes']

Array.from() konvertiert das von .entries() zurückgegebene Iterable in ein Array

assert.deepEqual(
  Array.from(map.entries()),
  [[false, 'no'], [true, 'yes']]);

Map-Instanzen sind auch Iterables über Einträge. Im folgenden Code verwenden wir Destrukturierung, um auf die Schlüssel und Werte von map zuzugreifen

for (const [key, value] of map) {
  console.log(key, value);
}
// Output:
// false, 'no'
// true, 'yes'

33.1.7 In Einfügungsreihenfolge aufgelistet: Einträge, Schlüssel, Werte

Maps zeichnen auf, in welcher Reihenfolge Einträge erstellt wurden, und respektieren diese Reihenfolge, wenn Einträge, Schlüssel oder Werte aufgelistet werden

const map1 = new Map([
  ['a', 1],
  ['b', 2],
]);
assert.deepEqual(
  Array.from(map1.keys()), ['a', 'b']);

const map2 = new Map([
  ['b', 2],
  ['a', 1],
]);
assert.deepEqual(
  Array.from(map2.keys()), ['b', 'a']);

33.1.8 Zwischen Maps und Objekten konvertieren

Solange eine Map nur Strings und Symbole als Schlüssel verwendet, können Sie sie in ein Objekt konvertieren (über Object.fromEntries())

const map = new Map([
  ['a', 1],
  ['b', 2],
]);
const obj = Object.fromEntries(map);
assert.deepEqual(
  obj, {a: 1, b: 2});

Sie können auch ein Objekt mit String- oder Symbolschlüsseln in eine Map konvertieren (über Object.entries())

const obj = {
  a: 1,
  b: 2,
};
const map = new Map(Object.entries(obj));
assert.deepEqual(
  map, new Map([['a', 1], ['b', 2]]));

33.2 Beispiel: Zeichen zählen

countChars() gibt eine Map zurück, die Zeichen auf die Anzahl ihrer Vorkommen abbildet.

function countChars(chars) {
  const charCounts = new Map();
  for (let ch of chars) {
    ch = ch.toLowerCase();
    const prevCount = charCounts.get(ch) ?? 0;
    charCounts.set(ch, prevCount+1);
  }
  return charCounts;
}

const result = countChars('AaBccc');
assert.deepEqual(
  Array.from(result),
  [
    ['a', 2],
    ['b', 1],
    ['c', 3],
  ]
);

33.3 Ein paar weitere Details zu den Schlüsseln von Maps (fortgeschritten)

Jeder Wert kann ein Schlüssel sein, auch ein Objekt

const map = new Map();

const KEY1 = {};
const KEY2 = {};

map.set(KEY1, 'hello');
map.set(KEY2, 'world');

assert.equal(map.get(KEY1), 'hello');
assert.equal(map.get(KEY2), 'world');

33.3.1 Welche Schlüssel gelten als gleich?

Die meisten Map-Operationen müssen prüfen, ob ein Wert gleich einem der Schlüssel ist. Dies geschieht über die interne Operation SameValueZero, die wie === funktioniert, aber NaN als gleich zu sich selbst betrachtet.

Daher können Sie NaN in Maps als Schlüssel verwenden, genau wie jeden anderen Wert

> const map = new Map();

> map.set(NaN, 123);
> map.get(NaN)
123

Unterschiedliche Objekte werden immer als unterschiedlich betrachtet. Das ist etwas, das (noch) nicht geändert werden kann – die Konfiguration der Schlüsselgleichheit steht auf der langfristigen Roadmap von TC39.

> new Map().set({}, 1).set({}, 2).size
2

33.4 Fehlende Map-Operationen

33.4.1 Maps abbilden und filtern

Sie können ein Array .map() und .filter(), aber es gibt keine solchen Operationen für eine Map. Die Lösung ist

  1. Konvertieren Sie die Map in ein Array von [Schlüssel, Wert]-Paaren.
  2. Bilden Sie das Array ab oder filtern Sie es.
  3. Konvertieren Sie das Ergebnis zurück in eine Map.

Ich werde die folgende Map verwenden, um zu demonstrieren, wie das funktioniert.

const originalMap = new Map()
.set(1, 'a')
.set(2, 'b')
.set(3, 'c');

Abbildung von originalMap

const mappedMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .map(([k, v]) => [k * 2, '_' + v]) // step 2
);
assert.deepEqual(
  Array.from(mappedMap),
  [[2,'_a'], [4,'_b'], [6,'_c']]);

Filterung von originalMap

const filteredMap = new Map( // step 3
  Array.from(originalMap) // step 1
  .filter(([k, v]) => k < 3) // step 2
);
assert.deepEqual(Array.from(filteredMap),
  [[1,'a'], [2,'b']]);

Array.from() konvertiert jedes Iterable in ein Array.

33.4.2 Maps kombinieren

Es gibt keine Methoden zum Kombinieren von Maps, weshalb wir einen Workaround verwenden müssen, der dem aus dem vorherigen Abschnitt ähnelt.

Kombinieren wir die folgenden beiden Maps

const map1 = new Map()
  .set(1, '1a')
  .set(2, '1b')
  .set(3, '1c')
;

const map2 = new Map()
  .set(2, '2b')
  .set(3, '2c')
  .set(4, '2d')
;

Um map1 und map2 zu kombinieren, erstellen wir ein neues Array und verteilen (...) die Einträge (Schlüssel-Wert-Paare) von map1 und map2 hinein (durch Iteration). Dann konvertieren wir das Array zurück in eine Map. All das geschieht in Zeile A

const combinedMap = new Map([...map1, ...map2]); // (A)
assert.deepEqual(
  Array.from(combinedMap), // convert to Array for comparison
  [ [ 1, '1a' ],
    [ 2, '2b' ],
    [ 3, '2c' ],
    [ 4, '2d' ] ]
);

  Übung: Zwei Maps kombinieren

exercises/maps/combine_maps_test.mjs

33.5 Schnellreferenz: Map<K,V>

Hinweis: Der Kürze halber tue ich so, als hätten alle Schlüssel denselben Typ K und alle Werte denselben Typ V.

33.5.1 Konstruktor

33.5.2 Map<K,V>.prototype: Umgang mit einzelnen Einträgen

33.5.3 Map<K,V>.prototype: Umgang mit allen Einträgen

33.5.4 Map<K,V>.prototype: Iterieren und Schleifen

Sowohl das Iterieren als auch das Schleifen erfolgen in der Reihenfolge, in der Einträge zu einer Map hinzugefügt wurden.

33.5.5 Quellen dieses Abschnitts

33.6 FAQ: Maps

33.6.1 Wann sollte ich eine Map und wann ein Objekt verwenden?

Wenn Sie eine wörterbuchähnliche Datenstruktur mit Schlüsseln benötigen, die weder Strings noch Symbole sind, haben Sie keine Wahl: Sie müssen eine Map verwenden.

Wenn Ihre Schlüssel jedoch entweder Strings oder Symbole sind, müssen Sie entscheiden, ob Sie ein Objekt verwenden möchten oder nicht. Eine grobe allgemeine Richtlinie lautet:

33.6.2 Wann würde ich ein Objekt als Schlüssel in einer Map verwenden?

Normalerweise möchten Sie, dass Map-Schlüssel nach Wert verglichen werden (zwei Schlüssel gelten als gleich, wenn sie den gleichen Inhalt haben). Das schließt Objekte aus. Es gibt jedoch einen Anwendungsfall für Objekte als Schlüssel: das externe Anhängen von Daten an Objekte. Aber dieser Anwendungsfall wird von WeakMaps besser bedient, bei denen Einträge Schlüssel nicht daran hindern, garbage collected zu werden (für Details siehe das nächste Kapitel).

33.6.3 Warum bewahren Maps die Einfügungsreihenfolge von Einträgen?

Prinzipiell sind Maps ungeordnet. Der Hauptgrund für die Ordnung von Einträgen ist, dass Operationen, die Einträge, Schlüssel oder Werte auflisten, deterministisch sind. Das hilft zum Beispiel beim Testen.

33.6.4center>Warum haben Maps eine .size, während Arrays eine .length haben?

In JavaScript haben indexierbare Sequenzen (wie Arrays und Strings) eine .length, während nicht-indexierte Sammlungen (wie Maps und Sets) eine .size haben.

  Quiz

Siehe Quiz-App.