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:
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);[ '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);[ '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 einerOrdereinen 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
sortsortiert das Array selbst und gibt es zurück.toSortedist 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
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
- Fünf Preise. values() gibt einen Iterator: einen Weg, durch den noch nichts gegangen ist.
- 12 besteht p >= 50 nicht und fällt heraus. Es kommt nie bei map an.
- 75 besteht filter, map macht daraus den Text „75 €“, toArray() sammelt ihn.
- Auch 40 fällt heraus. Jedes Element geht den ganzen Weg, bevor das nächste startet.
- 130 und 58 kommen durch. Das Array prices ändert sich nicht.
- 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());filter 12filter 75filter 40map 75[ 150 ]Der Iterator ist bereit, noch ist nichts hindurchgegangen.filter 12filter 75map 75filter 40[ 150 ]- Das Array (das erste
console.log) macht alles sofort und Schritt für Schritt:filtergeht über alle drei Preise, und erst dann gehtmapüber den einen, der übrig ist. - Der Iterator ist bereit, bevor auch nur ein Element durch ihn gelaufen ist. Dann kommt
map 75direkt nachfilter 75: Die Elemente warten nicht, bis alle durchfiltergelaufen sind. - Deshalb kannst du aus dem Iterator mit
takenur 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());filter 75filter 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}`);Summe der geraden Zahlen: 6reduce(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)} €`);Bestellungen ab 50 €: 3Umsatz: 312.40 €Durchschnitt: 62.48 €Größte: 120.00 €- Die
lengthdes gefilterten Arrays ist die Anzahl. - Der Durchschnitt ist die Summe geteilt durch die Anzahl. Gibt es keine Bestellungen, ist
0 / 0gleichNaN(„not a number“, keine Zahl), daher die Prüfungorders.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);0-InfinityNaNOhne 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}`));[ 2, 3, 4, 5 ][ 'Lukas', 'Maria', 'Anna' ]Tisch 1Tisch 2Tisch 3new 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 Indexidie 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)]);[ 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);new Set lässt kaffee, tee, wasser, saft übrig. toSorted() sortiert sie alphabetisch: kaffee, saft, tee, wasser. slice(0, 3) behält die ersten drei, und map schreibt sie groß.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");Größte: Lukasfalsekeine gefunden[0]eines leeren Arrays istundefined.find(Bedingung)liefert das erste Element, für das die Bedingung wahr ist, oderundefined.huge?.customer: Isthugegleichundefinedodernull, ist der ganze Ausdruckundefined, statt dass das Programm abbricht. Das heißt Optional Chaining.x ?? ylieferty, wennxgleichundefinedodernullist, und sonstx.
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");Spitzname: Mimikein SpitznameGenauer, für Neugierige
- Ohne
?.:
const huge = orders.find((o) => o.total > 500);console.log(huge.customer);TypeError: Cannot read properties of undefined (reading 'customer')x || yliefert ebenfalls einen Ersatzwert, behandelt aber auch einen leeren String und0als 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);kein Spitznamekein Spitzname
0someundeveryantworten mittrueoderfalse, 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));truetrueProbier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
sort macht kein neues Array. Es sortiert prices selbst und gibt es zurück, deshalb sind sorted und prices ein und dasselbe Array:
Teuerster: 130Zuerst eingegeben: 130Die Korrektur ist toSorted in Zeile 2. Es gibt ein neues sortiertes Array zurück, und prices bleibt 12, 75, 40, 130.
Der Iterator aus values() dagegen läuft nur einmal durch, wie ein Stream in Java. Das zweite toArray() liefert ein leeres Array:
const prices = [12, 75, 40, 130];const expensive = prices.values().filter((p) => p >= 50);console.log(expensive.toArray());console.log(expensive.toArray());[ 75, 130 ][]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);{ Berlin: 1, Dresden: 2, Hamburg: 2 }Berlin: 120.00 €Dresden: 76.90 €Hamburg: 115.50 €Anna, Laura, Lukas, Maria, TomMap.groupBy(Array, Funktion)liefert eineMap: für jede Stadt ein Array ihrer Bestellungen.[...byCity.keys()]macht ein Array aus den Schlüsseln, undtoSorted()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);Map(3) { 'Hamburg' => [ 'Maria', 'Laura' ], 'Dresden' => [ 'Tom', 'Anna' ], 'Berlin' => [ 'Lukas' ]}- Wiederholt sich in den Paaren für
Object.fromEntriesein Schlüssel, gewinnt der letzte Wert, ohne Fehler (in Java wirfttoMapeine Exception):
const lastByCity = Object.fromEntries(orders.map((o) => [o.city, o.customer]));console.log(lastByCity);{ Hamburg: 'Laura', Dresden: 'Anna', Berlin: 'Lukas' }Übung
Berichte mit Array-Methoden
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 €.
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].
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).
4.50 5.75 6.00 3.00 5.50Durchschnitt: 4.95Höchste: 6.00Ausgezeichnet: 34. 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.
Kaffee mit Milch, Kaffee ohne Zucker. Tee mit Honig!honig, kaffee, milch, mit, ohne, tee, zuckerWeitere Aufgaben
- Das längste Wort. Finde mit
reducedas 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.groupByund gib die Durchschnittsnote jeder Klasse aus. - Große und kleine Bestellungen. Prüfe mit
some, ob es eine Bestellung über 1000 € gibt, und mitevery, ob alle mindestens 10 € betragen.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Arrays: push, splice und sort · Map und Set · Klassen und Daten in Klassen · Pfeilfunktionen
- Geht weiter in: Daten, switch und Destrukturierung · Tests mit node:test · Datenbanken · Reflection
- Aufgaben zum Thema: Modernes JavaScript