mitmario.dev

Zusammenfassen mit reduce

JavaScript Im Browser 3 Min Lesezeit 4 BeispieleLektion 7 von 10

reduce ist die dritte der drei Methoden und die, vor der die meisten kapitulieren. Das liegt nicht an der Idee, sondern an der Signatur: Zwei Parameter im Callback plus ein Wert dahinter, und beim ersten Lesen ist keiner davon offensichtlich.

Deshalb hier einmal langsam.

Was reduce tut

reduce, Durchlauf für Durchlauf
const zahlen = [12, 19, 34, 17];

// So ginge es von Hand.
let summeVonHand = 0;
for (const zahl of zahlen) {
  summeVonHand = summeVonHand + zahl;
}
console.log("Von Hand:", summeVonHand);

// Und so mit reduce. Der erste Parameter ist der Zwischenstand,
// der zweite der aktuelle Eintrag, die 0 am Ende der Startwert.
const summe = zahlen.reduce((zwischenstand, zahl) => zwischenstand + zahl, 0);
console.log("Mit reduce:", summe);

// Und hier jeder Durchlauf einzeln, damit man es sieht.
zahlen.reduce((zwischenstand, zahl) => {
  const neu = zwischenstand + zahl;
  console.log(`${zwischenstand} plus ${zahl} ergibt ${neu}`);
  return neu;
}, 0);

map macht aus einer Liste eine gleich lange Liste. filter macht eine kürzere daraus. reduce macht einen einzelnen Wert daraus.

Dafür braucht es zwei Dinge: eine Funktion, die aus dem bisherigen Zwischenstand und dem nächsten Eintrag einen neuen Zwischenstand macht, und einen Startwert.

Der springende Punkt ist der Zwischenstand. Er ist immer das, was der Callback beim letzten Durchlauf zurückgegeben hat. Im ersten Durchlauf gibt es noch kein letztes Mal, deshalb der Startwert.

Der dritte Block im Beispiel schreibt jeden Durchlauf einzeln in die Console. Schau ihn dir an, bis die Kette klar ist: 0 plus 12 ergibt 12, 12 plus 19 ergibt 31, 31 plus 34 ergibt 65, 65 plus 17 ergibt 82. Der Rückgabewert des einen Durchlaufs ist der erste Parameter des nächsten.

Dieser dritte Block ist übrigens nur deshalb so ausführlich, weil er sich selbst beim Rechnen zusehen soll. Für deinen eigenen Code brauchst du diese Zeilen nicht: Zeichne im Reiter Debug auf, und dieselbe Kette steht da, an den beiden ersten Blöcken und ohne ein einziges console.log. Blätter zu einem Schritt im Callback, und im Werteblock stehen zwischenstand und zahl nebeneinander, in jedem Durchlauf mit anderen Zahlen. Genau darum geht es in dieser Lektion.

Und wie bei map gilt: Vergisst du das return im Callback, ist der Zwischenstand ab dem nächsten Durchlauf undefined.

Der Startwert

Der Startwert ist keine Formalie
const zahlen = [12, 19, 34];

// Mit Startwert: der erste Durchlauf beginnt bei 0.
console.log("Mit 0:", zahlen.reduce((a, b) => a + b, 0));

// Mit einem anderen Startwert rechnet er einfach weiter.
console.log("Mit 100:", zahlen.reduce((a, b) => a + b, 100));

// Ohne Startwert nimmt reduce den ersten Eintrag als Anfang und
// beginnt beim zweiten. Bei einer Summe fällt das nicht auf.
console.log("Ohne:", zahlen.reduce((a, b) => a + b));

// Bei einer leeren Liste mit Startwert kommt der Startwert heraus.
console.log("Leer mit 0:", [].reduce((a, b) => a + b, 0));

// Und ohne Startwert bricht es ab. Diese Zeile steht deshalb
// ganz unten.
console.log("Leer ohne:", [].reduce((a, b) => a + b));

Der Startwert ist technisch optional, und trotzdem gehört er hin.

Lässt du ihn weg, nimmt reduce den ersten Eintrag als Anfang und beginnt beim zweiten. Bei einer Summe fällt das nicht auf, das Ergebnis ist dasselbe.

Bei einer leeren Liste fällt es auf: Ohne Startwert gibt es keinen ersten Eintrag, den man nehmen könnte, und reduce bricht mit einem TypeError ab. Mit Startwert kommt einfach der Startwert heraus, und das ist fast immer die richtige Antwort.

Schreib ihn deshalb immer hin. Eine leere Liste ist kein Sonderfall, sondern der Normalfall am Anfang: Der Warenkorb ist leer, die Suche hat nichts gefunden, die Daten sind noch nicht da.

Der Startwert bestimmt nebenbei auch den Typ des Ergebnisses. 0 für eine Summe, "" für einen Text, [] für eine Liste.

Nicht nur Summen

Das größte Element finden
const zahlen = [12, 19, 34, 17];

// Der Zwischenstand muss keine Summe sein. Hier ist er die
// größte Zahl, die bisher aufgetaucht ist.
const groesste = zahlen.reduce(
  (bisher, zahl) => (zahl > bisher ? zahl : bisher),
  zahlen[0]
);

console.log("Größte:", groesste);

// Als Startwert der erste Eintrag, nicht die 0: bei lauter
// negativen Zahlen wäre die 0 sonst das Ergebnis.
const negative = [-5, -12, -3];
console.log("Mit 0 als Start:", negative.reduce((a, b) => (b > a ? b : a), 0));
console.log("Mit erstem Eintrag:", negative.reduce((a, b) => (b > a ? b : a), negative[0]));

Der Zwischenstand muss keine Summe sein. Er kann alles sein, was sich Schritt für Schritt aufbauen lässt.

Beim Maximum ist er die größte Zahl, die bisher aufgetaucht ist. Der Callback vergleicht und gibt die größere der beiden zurück, mehr nicht.

Der Startwert ist hier der erste Eintrag und nicht die 0. Mit 0 bekämest du bei lauter negativen Zahlen die 0 als Ergebnis, und die steht gar nicht in der Liste. Das ist ein Fehler, der bei den Testdaten nie auftritt und in der Praxis sofort.

Einen Text aufbauen

Einen Text zusammenbauen
const zahlen = [12, 19, 34, 17];

// Der Zwischenstand darf auch ein Text sein. Der Startwert ist
// dann der leere Text.
const naiv = zahlen.reduce((text, zahl) => `${text}-${zahl}`, "");
console.log("Naiv:", naiv);

// Der Bindestrich vorn ist das übliche Problem. Beim ersten
// Durchlauf ist der Zwischenstand noch leer, also lässt man ihn
// dort weg.
const sauber = zahlen.reduce(
  (text, zahl) => (text === "" ? String(zahl) : `${text}-${zahl}`),
  ""
);
console.log("Sauber:", sauber);

// Für genau diesen Fall gibt es eine fertige Methode, und die
// nimmt man auch. reduce ist für alles, wofür es keine gibt.
console.log("Mit join:", zahlen.join("-"));

Auch ein Text lässt sich Stück für Stück zusammensetzen. Startwert ist dann der leere Text.

Dabei tritt ein kleines Problem auf, das dir überall begegnen wird: Das Trennzeichen gehört zwischen die Einträge, nicht davor. Beim ersten Durchlauf ist der Zwischenstand noch leer, also lässt man es dort weg.

Für genau diesen Fall gibt es allerdings join, und das nimmt man auch. reduce ist für alles, wofür es keine fertige Methode gibt. Es ist mächtig genug, um map, filter und join nachzubauen, und keins davon sollte man damit nachbauen.

In der Challenge baust du den Text trotzdem einmal mit reduce, weil man die Mechanik dabei sieht. Danach darfst du join nehmen.

Zum Mitnehmen

Der Zwischenstand ist immer das, was der Callback beim letzten Durchlauf zurückgegeben hat. Wer das einmal verstanden hat, hat reduce verstanden.

Jetzt du

Basis Konto, kostenlos

Zu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.

Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.

Was in dieser Lektion steckt

  • Artikel mit 4 Beispielen zum Ausprobieren

    Steht hier, ohne Konto lesbar.

  • Aufgabe im Editor, direkt im Browser geprüft

    Öffnet sich mit dem Basis Konto.