Zum Inhalt
wannacode

Array-Methoden

Kurz gesagt: Die Array-Methoden verarbeiten Daten als Folge von Schritten: filter siebt aus, map wandelt um, toSorted sortiert, und reduce baut ein einziges Ergebnis.

Problem

Ein Bericht in einer Schleife

Die Bestellungen haben jetzt auch eine Stadt:

Order
class Order {
constructor(customer, city, total) {
this.customer = customer;
this.city = city;
this.total = total;
}
}
const orders = [
new Order("Maria", "Hamburg", 75.5),
new Order("Tom", "Dresden", 12.9),
new Order("Laura", "Hamburg", 40),
new Order("Lukas", "Berlin", 120),
new Order("Anna", "Dresden", 64),
];

Du willst die Kunden mit Bestellungen ab 50 €, alphabetisch sortiert:

const result = [];
for (const o of orders) {
if (o.total >= 50) {
result.push(o.customer);
}
}
result.sort();
console.log(result);
Ausgabe
[ 'Anna', 'Lukas', 'Maria' ]

Acht Zeilen, und die Frage ist nur eine. Der Code sagt, wie: eine leere Liste anlegen, durchlaufen, prüfen, hinzufügen, sortieren. Um zu verstehen, was er tut, musst du ihn im Kopf ausführen. Kommt „nur die ersten drei“ oder „ohne Wiederholungen“ dazu, wächst die Schleife weiter.

Die Idee

Eine Kette von Schritten

Derselbe Bericht, als Schritte geschrieben:

const result = orders
.filter((o) => o.total >= 50)
.map((o) => o.customer)
.toSorted();
console.log(result);
Ausgabe
[ 'Anna', 'Lukas', 'Maria' ]

Du liest ihn von oben nach unten: Nimm die Bestellungen, behalte die ab 50 €, nimm den Kunden, sortiere. Jeder Schritt ist eine Methode des Arrays und nimmt eine Pfeilfunktion aus der vorigen Lektion:

  • filter(Bedingung) behält nur die Elemente, für die die Bedingung wahr ist;
  • map(Funktion) macht aus jedem Element ein anderes: aus einer Order einen String;
  • toSorted() sortiert alphabetisch, toSorted(Vergleichsfunktion) nach deiner Regel.

Jede Methode gibt ein neues Array zurück, und die nächste wird darauf aufgerufen. Deshalb reihen sich die Schritte zu einer Kette. Das Array orders ändert sich nicht.

Genauer, für Neugierige
  • Das alte sort sortiert das Array selbst und gibt es zurück. toSorted ist das neuere und gibt ein neues Array zurück. Warum der Unterschied wichtig ist, siehst du in der Karte „Finde den Fehler“.
  • Jeder Schritt läuft über das ganze Array, bevor der nächste beginnt. Ein Förderband, auf dem die Elemente eins nach dem anderen laufen, bekommst du mit einem Iterator. Um ihn geht es in der nächsten Karte.

Sieh es dir an

Was durch den Filter kommt

Hier sind die Schritte nicht auf dem Array, sondern auf dem Iterator, den values() liefert. Der Iterator hat dieselben filter und map, aber er ist ein Förderband: Er liefert die Werte einzeln nacheinander, wenn jemand nach ihnen fragt. toArray() sammelt sie in einem Array.

const prices = [12, 75, 40, 130, 58];
const labels = prices
.values()
.filter((p) => p >= 50)
.map((p) => `${p} €`)
.toArray();
console.log(labels);
Schritt für Schritt, in Worten
  1. Fünf Preise. values() gibt einen Iterator: einen Weg, durch den noch nichts gegangen ist.
  2. 12 besteht p >= 50 nicht und fällt heraus. Es kommt nie bei map an.
  3. 75 besteht filter, map macht daraus den Text „75 €“, toArray() sammelt ihn.
  4. Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
  5. 130 und 58 kommen durch. Das Array prices ändert sich nicht.
  6. Das Ergebnis ist ein neues Array mit drei Strings.

Die fünf Preise laufen einer nach dem anderen. 12 kommt bei filter an und fällt heraus. 75 kommt durch, map macht daraus den Text „75 €“, und toArray() sammelt ihn. Dann fällt 40 heraus, und 130 und 58 kommen durch.

Jedes Element geht den ganzen Weg, bevor das nächste startet. Und nichts startet, bis toArray() nach den Werten fragt.

Genauer, für Neugierige

Das heißt Lazy Evaluation (bedarfsgesteuerte Auswertung). Die Methoden des Iterators merken sich nur, was zu tun ist. Sieh es dir mit einer Ausgabe in jedem Schritt an:

const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
const double = (p) => {
console.log("map " + p);
return p * 2;
};
console.log([12, 75, 40].filter(check).map(double));
const stream = [12, 75, 40].values().filter(check).map(double);
console.log("Der Iterator ist bereit, noch ist nichts hindurchgegangen.");
console.log(stream.toArray());
Ausgabe
filter 12
filter 75
filter 40
map 75
[ 150 ]
Der Iterator ist bereit, noch ist nichts hindurchgegangen.
filter 12
filter 75
map 75
filter 40
[ 150 ]
  • Das Array (das erste console.log) macht alles sofort und Schritt für Schritt: filter geht über alle drei Preise, und erst dann geht map über den einen, der übrig ist.
  • Der Iterator ist bereit, bevor auch nur ein Element durch ihn gelaufen ist. Dann kommt map 75 direkt nach filter 75: Die Elemente warten nicht, bis alle durch filter gelaufen sind.
  • Deshalb kannst du aus dem Iterator mit take nur die ersten paar Werte nehmen, ohne die übrigen zu prüfen:
const check = (p) => {
console.log("filter " + p);
return p >= 50;
};
console.log([75, 130, 58, 12, 99].values().filter(check).take(2).toArray());
Ausgabe
filter 75
filter 130
[ 75, 130 ]

Für alltägliche Aufgaben reicht das Array, und es liest sich leichter. Der Iterator ist für viele Daten, oder wenn du nur die ersten paar brauchst.

Die Idee

Summe, Anzahl, Durchschnitt

Das Ergebnis kann statt einer Liste auch eine Zahl sein. Die Summe der geraden Zahlen:

const numbers = [1, 2, 3, 4, 5];
const sum = numbers.filter((n) => n % 2 === 0).reduce((total, n) => total + n, 0);
console.log(`Summe der geraden Zahlen: ${sum}`);
Ausgabe
Summe der geraden Zahlen: 6
reduce(Funktion, Start) macht aus dem Array einen einzigen Wert. Es beginnt bei 0, und die Funktion bekommt die bisherige Summe und das nächste Element und gibt die neue Summe zurück.

Dasselbe für die Bestellungen:

const big = orders.filter((o) => o.total >= 50).length;
const revenue = orders.reduce((sum, o) => sum + o.total, 0);
const average = orders.length > 0 ? revenue / orders.length : 0;
const maxTotal = Math.max(...orders.map((o) => o.total));
console.log(`Bestellungen ab 50 €: ${big}`);
console.log(`Umsatz: ${revenue.toFixed(2)} €`);
console.log(`Durchschnitt: ${average.toFixed(2)} €`);
console.log(`Größte: ${maxTotal.toFixed(2)} €`);
Ausgabe
Bestellungen ab 50 €: 3
Umsatz: 312.40 €
Durchschnitt: 62.48 €
Größte: 120.00 €
  • Die length des gefilterten Arrays ist die Anzahl.
  • Der Durchschnitt ist die Summe geteilt durch die Anzahl. Gibt es keine Bestellungen, ist 0 / 0 gleich NaN („not a number“, keine Zahl), daher die Prüfung orders.length > 0.
  • Math.max(...array) liefert die größte Zahl. ... verteilt das Array auf einzelne Parameter: Math.max(75.5, 12.9, ...). Ist das Array leer, ist das Ergebnis -Infinity. Was du mit einem Ergebnis machst, das fehlen kann, ist die nächste Idee.
Genauer, für Neugierige

Mit einem leeren Array:

const prices = [];
console.log(prices.reduce((sum, p) => sum + p, 0));
console.log(Math.max(...prices));
console.log(0 / prices.length);
Ausgabe
0
-Infinity
NaN

Ohne Startwert bricht reduce auf einem leeren Array mit TypeError: Reduce of empty array with no initial value ab. Schreib den Startwert deshalb immer dazu.

Die Idee

Sortieren, abschneiden, ohne Wiederholungen

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)].toSorted((a, b) => a - b));
const top = orders
.toSorted((a, b) => b.total - a.total)
.slice(0, 3)
.map((o) => o.customer);
console.log(top);
Array.from({ length: 3 }, (_, i) => i + 1).forEach((i) => console.log(`Tisch ${i}`));
Ausgabe
[ 2, 3, 4, 5 ]
[ 'Lukas', 'Maria', 'Anna' ]
Tisch 1
Tisch 2
Tisch 3
  • new Set(ratings) entfernt die Wiederholungen: In einem Set (einer Menge) kommt jeder Wert nur einmal vor. [...set] macht daraus wieder ein Array.
  • toSorted((a, b) => b.total - a.total) sortiert ab der größten Bestellung, mit einer Vergleichsfunktion aus der vorigen Lektion.
  • slice(0, 3) nimmt die Elemente von Index 0 bis Index 3, ohne ihn: die ersten drei. Zusammen mit dem Sortieren ist das eine „Top 3“.
  • JavaScript hat kein range. Array.from({ length: 3 }, (_, i) => i + 1) macht ein Array mit 3 Elementen und ruft dabei für jeden Index i die Funktion auf: 1, 2, 3. _ ist der Name eines Parameters, den wir nicht brauchen.
Genauer, für Neugierige

Ein Set behält die Werte in der Reihenfolge des Hinzufügens, deshalb bekommst du ohne Sortieren die Reihenfolge des ersten Auftretens:

const ratings = [5, 3, 5, 4, 2, 4, 5];
console.log([...new Set(ratings)]);
Ausgabe
[ 5, 3, 4, 2 ]

Probier es aus

Was kommt am Ende an?

Was gibt das Programm aus?

const drinks = ["kaffee", "tee", "kaffee", "wasser", "tee", "saft"];
const result = [...new Set(drinks)]
.toSorted()
.slice(0, 3)
.map((d) => d.toUpperCase());
console.log(result);
Antwort

Die Idee

undefined: ein Ergebnis, das fehlen kann

Welche Bestellung ist die größte? Ist das Array leer, gibt es keine Antwort. Dann ist das Ergebnis undefined: „kein Wert“. Aber der Aufrufer muss daran denken, es zu prüfen. Vergisst er es, bricht das Programm irgendwann später mit einem TypeError ab.

Java hat dafür Optional: eine Box, die entweder einen Wert enthält oder leer ist. JavaScript hat keine solche Box, aber zwei Operatoren, mit denen die Prüfung kurz wird:

const biggest = orders.toSorted((a, b) => b.total - a.total)[0];
if (biggest !== undefined) {
console.log(`Größte: ${biggest.customer}`);
}
const huge = orders.find((o) => o.total > 500);
console.log(huge !== undefined);
console.log(huge?.customer ?? "keine gefunden");
Ausgabe
Größte: Lukas
false
keine gefunden
  • [0] eines leeren Arrays ist undefined.
  • find(Bedingung) liefert das erste Element, für das die Bedingung wahr ist, oder undefined.
  • huge?.customer: Ist huge gleich undefined oder null, ist der ganze Ausdruck undefined, statt dass das Programm abbricht. Das heißt Optional Chaining.
  • x ?? y liefert y, wenn x gleich undefined oder null ist, und sonst x.

Deine eigenen Funktionen geben meist null zurück, wenn sie keine Antwort haben: „fehlt mit Absicht“. ?? funktioniert auch damit:

function nickname(user) {
// Gibt den Spitznamen zurück oder null, wenn die Person keinen hat.
return user === "maria" ? "Mimi" : null;
}
const maria = nickname("maria");
if (maria !== null) {
console.log(`Spitzname: ${maria}`);
}
const tom = nickname("tom");
console.log(tom ?? "kein Spitzname");
Ausgabe
Spitzname: Mimi
kein Spitzname
Genauer, für Neugierige
  • Ohne ?.:
const huge = orders.find((o) => o.total > 500);
console.log(huge.customer);
Fehler
TypeError: Cannot read properties of undefined (reading 'customer')
  • x || y liefert ebenfalls einen Ersatzwert, behandelt aber auch einen leeren String und 0 als fehlend. ?? tut das nicht (die dritte Zeile ist ein leerer String):
console.log(null ?? "kein Spitzname");
console.log("" || "kein Spitzname");
console.log("" ?? "kein Spitzname");
console.log(0 ?? 5);
Ausgabe
kein Spitzname
kein Spitzname
0
  • some und every antworten mit true oder false, ob irgendein oder jedes Element die Bedingung erfüllt:
console.log(orders.some((o) => o.total > 100));
console.log(orders.every((o) => o.total >= 10));
Ausgabe
true
true

Probier es aus

Finde den Fehler

Das Programm soll den teuersten Preis ausgeben und den zuerst eingegebenen Preis, 12. Der zweite kommt falsch heraus. In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Gruppieren und verbinden

Wie viele Bestellungen gibt es aus jeder Stadt, und wie hoch ist der Umsatz?

const byCity = Map.groupBy(orders, (o) => o.city);
const cities = [...byCity.keys()].toSorted();
const countByCity = Object.fromEntries(cities.map((city) => [city, byCity.get(city).length]));
console.log(countByCity);
for (const city of cities) {
const sum = byCity.get(city).reduce((total, o) => total + o.total, 0);
console.log(`${city}: ${sum.toFixed(2)} €`);
}
const customers = orders
.map((o) => o.customer)
.toSorted()
.join(", ");
console.log(customers);
Ausgabe
{ Berlin: 1, Dresden: 2, Hamburg: 2 }
Berlin: 120.00 €
Dresden: 76.90 €
Hamburg: 115.50 €
Anna, Laura, Lukas, Maria, Tom
  • Map.groupBy(Array, Funktion) liefert eine Map: für jede Stadt ein Array ihrer Bestellungen.
  • [...byCity.keys()] macht ein Array aus den Schlüsseln, und toSorted() sortiert sie alphabetisch.
  • Object.fromEntries(Paare) macht ein Objekt aus [schlüssel, wert]-Paaren. Hier ist der Schlüssel die Stadt und der Wert die Anzahl ihrer Bestellungen (length).
  • join(", ") verbindet die Strings zu einem, mit einem Komma dazwischen.

Eine Map behält die Schlüssel in der Reihenfolge des Hinzufügens: Hamburg, Dresden, Berlin. Deshalb werden die Städte ausdrücklich sortiert.

Genauer, für Neugierige
  • Für jede Stadt ein Array der Namen. Hier stehen die Städte in der Reihenfolge des Hinzufügens:
const namesByCity = new Map();
for (const [city, list] of Map.groupBy(orders, (o) => o.city)) {
namesByCity.set(city, list.map((o) => o.customer));
}
console.log(namesByCity);
Ausgabe
Map(3) {
'Hamburg' => [ 'Maria', 'Laura' ],
'Dresden' => [ 'Tom', 'Anna' ],
'Berlin' => [ 'Lukas' ]
}
  • Wiederholt sich in den Paaren für Object.fromEntries ein Schlüssel, gewinnt der letzte Wert, ohne Fehler (in Java wirft toMap eine Exception):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));
console.log(lastByCity);
Ausgabe
{ Hamburg: 'Laura', Dresden: 'Anna', Berlin: 'Lukas' }

Übung

Berichte mit Array-Methoden

Löse die Aufgaben in deinem Editor. Lies die Eingabe wie bisher mit ask.

1. Verkäufe nach Kategorie. Eine Klasse Sale mit Produkt, Kategorie, Menge und Stückpreis und einer Methode revenue() (Menge mal Preis). Gruppiere mit Map.groupBy nach Kategorie, addiere jede Gruppe mit reduce, gib die Kategorien alphabetisch und den Gesamtumsatz aus. Die Verkäufe: Kaffeebohnen (Kategorie „Kaffee und Tee“), 12 Stück zu 18,50 €; Tee („Kaffee und Tee“), 30 zu 4,20 €; Schokolade („Snacks“), 40 zu 2,80 €; Kekse („Snacks“), 25 zu 3,10 €; Honig („Feinkost“), 8 zu 12 €; noch einmal Kaffeebohnen, 5 zu 18,50 €.

Ausgabe
Feinkost: 96.00 €
Kaffee und Tee: 440.50 €
Snacks: 189.50 €
Gesamt: 726.00 €

2. Die Top 3 der Produkte. Finde mit denselben Verkäufen die drei Produkte mit dem höchsten Umsatz. Der Kaffee steht in zwei Verkäufen, deshalb gruppierst du zuerst nach Produkt und addierst, dann sortierst du die Einträge der Map nach dem Wert. Tipp: [...byProduct].toSorted((a, b) => b[1] - a[1]).slice(0, 3): Jedes Element einer Map ist ein Paar [Produkt, Umsatz].

Ausgabe
Kaffeebohnen: 314.50 €
Tee: 126.00 €
Schokolade: 112.00 €

3. Die Durchschnittsnote. Lies eine Zeile mit Noten, getrennt durch Leerzeichen. Gib den Durchschnitt aus, die höchste Note und wie viele ausgezeichnet sind (5,50 und mehr). Tipp: (await ask("")).split(" ").map(Number).

Eingabe
4.50 5.75 6.00 3.00 5.50
Ausgabe
Durchschnitt: 4.95
Höchste: 6.00
Ausgezeichnet: 3

4. Die Wörter in einem Text. Lies eine Zeile Text und gib die Wörter darin ohne Wiederholungen aus, alphabetisch, kleingeschrieben, durch Kommas getrennt. Teile an allem, was kein Buchstabe ist: split(/[^\p{L}]+/u), und entferne die leeren Strings.

Eingabe
Kaffee mit Milch, Kaffee ohne Zucker. Tee mit Honig!
Ausgabe
honig, kaffee, milch, mit, ohne, tee, zucker

Weitere Aufgaben

  • Das längste Wort. Finde mit reduce das längste Wort in einem Satz. Was passiert bei einer leeren Zeile ohne Startwert?
  • Durchschnitt pro Klasse. Schüler mit Name, Klasse und Note: Gruppiere sie mit Map.groupBy und gib die Durchschnittsnote jeder Klasse aus.
  • Große und kleine Bestellungen. Prüfe mit some, ob es eine Bestellung über 1000 € gibt, und mit every, ob alle mindestens 10 € betragen.

Test

Teste dich

1. Was passiert mit dem Array orders nach orders.filter((o) => o.total > 50)?
2. Welche dieser Methoden ändert das Array selbst?
3. orders.find((o) => o.total > 500) findet keine Bestellung. Was gibt es zurück?
4. Was ergibt huge?.customer ?? "keine gefunden", wenn huge undefined ist?
5. orders.reduce((sum, o) => sum + o.total, 0). Was ist die 0?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·