Funktionen und Arrays
Auftrag
Martina Hostettler bereitet die nächste Vorstandssitzung vor und hat zwei Wünsche aufgeschrieben.
Als Vorstand möchte ich wissen, wie viele Dinge wir in jeder Kategorie haben, damit wir entscheiden können, wo wir als Nächstes Dinge anschaffen.
Als Freiwillige am Tresen möchte ich sehen, welches Werkzeug gerade da ist, nach Namen sortiert, damit ich Mitgliedern am Telefon schnell antworten kann.
Die Akzeptanzkriterien lauten:
- Die Übersicht zeigt für jede Kategorie die Anzahl Dinge.
- Sie nennt das Werkzeug, das da ist, alphabetisch sortiert, auch Namen mit Umlauten am richtigen Platz.
- Der Katalog selbst bleibt dabei unverändert.
Lernziele
Nach diesem Kapitel kannst du
- Funktionen als Argumente übergeben und Callbacks lesen,
- Listen mit
map,filter,find,someundeveryauswerten, - mit
reduceSummen und Zählungen berechnen, - Listen mit
toSortedundlocaleComparesortieren, ohne das Original zu ändern, - Funktionen schreiben, die andere Funktionen bauen.
Pfeilfunktionen und Closures kennst du aus der Einheit Funktionen und Gültigkeitsbereich der JS-Werkstatt.
Funktionen als Argumente
Abschnitt betitelt „Funktionen als Argumente“Eine Funktion, die du einer anderen Funktion übergibst, damit diese sie aufruft, heisst Callback. Du kennst das von addEventListener. Auch Arrays haben Methoden, die für jedes Element einen Callback aufrufen.
function forEachItem(items, callback) { for (const item of items) callback(item)}
const items = [{ name: 'Bohrmaschine' }, { name: 'Leiter' }]
forEachItem(items, (item) => console.log(item.name))items.forEach((item, index) => console.log(index, item.name))forEachItem zeigt, was dabei passiert. Die Schleife steckt in der Funktion, und der Callback sagt, was mit jedem Element geschehen soll. forEach ist die eingebaute Variante und übergibt dem Callback neben dem Element auch seine Stelle. Die folgenden Methoden funktionieren alle nach diesem Muster, sie unterscheiden sich darin, was sie mit den Ergebnissen des Callbacks machen.
map und filter
Abschnitt betitelt „map und filter“map ruft den Callback für jedes Element auf und baut aus den Rückgabewerten eine neue Liste, gleich lang wie die alte. filter behält die Elemente, für die der Callback true liefert.
const items = [ { id: 1, name: 'Bohrmaschine', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 2, name: 'Zelt für vier Personen', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Stichsäge', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 11, name: 'Akkuschrauber', category: 'Werkzeug', deposit: 20, availableFrom: '2026-11-05' },]
const ids = items.map((item) => item.id)console.log(ids)
const available = items.filter((item) => item.availableFrom === null)console.log(available.length)
const toolsThatAreIn = items .filter((item) => item.category === 'Werkzeug' && item.availableFrom === null) .map((item) => item.name)console.log(toolsThatAreIn)Beide Methoden ändern die alte Liste nicht, sie liefern eine neue. Darum lassen sie sich verketten. Die letzte Anweisung filtert zuerst das Werkzeug, das da ist, und macht aus den übrigen Dingen eine Liste von Namen. Diese Kette ist die Hälfte des zweiten Wunsches von Martina Hostettler.
find, some und every
Abschnitt betitelt „find, some und every“Manchmal braucht es nur ein Element oder eine Antwort mit Ja oder Nein.
const items = [ { id: 1, name: 'Bohrmaschine', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 2, name: 'Zelt für vier Personen', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Stichsäge', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 11, name: 'Akkuschrauber', category: 'Werkzeug', deposit: 20, availableFrom: '2026-11-05' },]
const tent = items.find((item) => item.id === 2)console.log(tent.name, items.findIndex((item) => item.id === 2))console.log(items.find((item) => item.id === 99))console.log(items.some((item) => item.deposit > 40), items.every((item) => item.deposit >= 20))find liefert das erste Element, für das der Callback true liefert, oder undefined, wenn keines passt. findIndex liefert die Stelle oder -1. some fragt, ob mindestens ein Element passt, every, ob alle passen. Alle vier hören auf, sobald die Antwort feststeht, und prüfen nicht mehr Elemente als nötig.
reduce fasst eine Liste zu einem einzigen Wert zusammen. Der Callback bekommt das bisherige Ergebnis und das nächste Element und gibt das neue Ergebnis zurück. Das zweite Argument von reduce ist der Startwert.
const items = [ { id: 1, name: 'Bohrmaschine', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 2, name: 'Zelt für vier Personen', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Stichsäge', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 11, name: 'Akkuschrauber', category: 'Werkzeug', deposit: 20, availableFrom: '2026-11-05' },]
const total = items.reduce((sum, item) => sum + item.deposit, 0)console.log(total)
const perCategory = items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts}, {})console.log(perCategory)
const groups = Object.groupBy(items, (item) => item.category)console.log(Object.keys(groups), groups['Werkzeug'].length)Die erste Zählung beginnt bei 0 und zählt alle Pfänder zusammen. Die zweite beginnt mit einem leeren Objekt und zählt für jede Kategorie mit, wie oft sie vorkommt. counts[item.category] ?? 0 liefert 0, solange eine Kategorie noch fehlt. Seit 2024 gibt es für das Gruppieren auch Object.groupBy, das die Dinge einer Kategorie als Liste sammelt. Für eine Zählung genügt reduce.
Sortieren ohne Nebenwirkung
Abschnitt betitelt „Sortieren ohne Nebenwirkung“sort sortiert ein Array an Ort und Stelle und ändert damit das Original. toSorted liefert eine sortierte Kopie. Wie verglichen wird, sagt eine Vergleichsfunktion. Sie liefert eine negative Zahl, wenn a vor b kommt, eine positive, wenn b vor a kommt, und 0, wenn beide gleich sind.
const items = [ { name: 'Stichsäge', deposit: 20 }, { name: 'Zelt für vier Personen', deposit: 50 }, { name: 'Akkuschrauber', deposit: 20 }, { name: 'Beamer', deposit: 80 },]
const byName = items.toSorted((a, b) => a.name.localeCompare(b.name, 'de-CH'))console.log(byName.map((item) => item.name))
const byDeposit = items.toSorted((a, b) => b.deposit - a.deposit)console.log(byDeposit.map((item) => item.deposit))console.log(items[0].name)localeCompare vergleicht Text nach den Regeln einer Sprache und setzt Umlaute an die richtige Stelle. Für Zahlen genügt eine Subtraktion, b.deposit - a.deposit sortiert absteigend. Die letzte Zeile zeigt, dass das Original seine Reihenfolge behalten hat.
Funktionen, die Funktionen bauen
Abschnitt betitelt „Funktionen, die Funktionen bauen“Eine Funktion kann eine Funktion zurückgeben, die sich ihre Argumente merkt. So entsteht ein Callback für filter nach Mass.
// Returns a function that remembers the category it was made forfunction byCategory(category) { return (item) => item.category === category}
const items = [ { name: 'Bohrmaschine', category: 'Werkzeug' }, { name: 'Zelt für vier Personen', category: 'Camping' }, { name: 'Stichsäge', category: 'Werkzeug' },]
const isTool = byCategory('Werkzeug')console.log(items.filter(isTool).length, items.filter(byCategory('Camping')).length)byCategory('Werkzeug') liefert eine neue Funktion, die nur noch ein Ding braucht und die Kategorie aus ihrer Closure kennt. Solche Funktionen machen Code lesbar, items.filter(isTool) sagt, was es tut. In der Knobelei baust du so einen Filter aus mehreren Kriterien.
Im Code
Abschnitt betitelt „Im Code“Arbeite mit dem Beispiel zu reduce. Lies den Code und schreib auf, was du erwartest, bevor du ihn ausführst.
const items = [ { id: 1, name: 'Bohrmaschine', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 2, name: 'Zelt für vier Personen', category: 'Camping', deposit: 50, availableFrom: '2026-11-12' }, { id: 5, name: 'Stichsäge', category: 'Werkzeug', deposit: 20, availableFrom: null }, { id: 11, name: 'Akkuschrauber', category: 'Werkzeug', deposit: 20, availableFrom: '2026-11-05' },]
const total = items.reduce((sum, item) => sum + item.deposit, 0)console.log(total)
const perCategory = items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts}, {})console.log(perCategory)
const groups = Object.groupBy(items, (item) => item.category)console.log(Object.keys(groups), groups['Werkzeug'].length)- Was gibt die erste Zählung aus, wenn du den Startwert 0 weglässt? Und was die zweite ohne
{}? - Ändere die zweite Zählung so, dass sie für jede Kategorie die Summe der Pfänder liefert statt der Anzahl.
- Wie sieht die Zählung aus, wenn du sie mit einer Schleife
for...ofschreibst? Was ist leichter zu lesen? - Was gibt
[].reduce((sum, item) => sum + item.deposit)aus, ohne Startwert und mit einer leeren Liste?
Ohne Startwert nimmt reduce das erste Element als Startwert. Bei der ersten Zählung ist das ein Objekt, und die Summe wird zu Text. Bei der zweiten zählt reduce in die Bohrmaschine hinein, ändert sie dabei und lässt sie selbst in der Zählung aus. Bei einer leeren Liste meldet reduce ohne Startwert einen TypeError. Gib den Startwert deshalb immer an.
Aufgaben
Abschnitt betitelt „Aufgaben“Die Aufgaben bauen die Auswertungen für die Vorstandssitzung. Du findest das Projekt im Kursrepo im Ordner exercises/01-javascript/arrays/de, mit dem ganzen Katalog in data/items.json. Kopiere den Ordner, installiere die Pakete und starte die Tests.
npm installnpm testMit npm run overview siehst du die Übersicht, die deine Funktionen verwendet. Zu Beginn ist sie leer.
Aufgabe Einstieg
Schreib in src/catalog.js die Funktion availableNames(items, category). Sie liefert die Namen der Dinge einer Kategorie, die da sind, nach den Regeln der Sprache sortiert. Für das Werkzeug sind es acht Namen, von der Bohrmaschine bis zum Werkzeugkoffer.
Lösung
// Names of the things of one category that are in, sorted by nameexport function availableNames(items, category) { return items .filter((item) => item.category === category && item.availableFrom === null) .map((item) => item.name) .toSorted((a, b) => a.localeCompare(b, 'de-CH'))}filter behält das Werkzeug, das da ist, map macht daraus Namen, und toSorted sortiert sie mit localeCompare. Ohne localeCompare stünde die Ölkanne aus dem zweiten Test nach der Zange, weil das Ö einen höheren Code hat als das Z.
Aufgabe Vertiefung
Schreib die Funktion countByCategory(items). Sie liefert ein Objekt, das für jede Kategorie die Anzahl Dinge nennt. Im Katalog sind es zehn pro Kategorie.
Lösung
// How many things there are per category, as an object with the names of the categories as keysexport function countByCategory(items) { return items.reduce((counts, item) => { counts[item.category] = (counts[item.category] ?? 0) + 1 return counts }, {})}Der Startwert ist ein leeres Objekt, und jede Runde zählt bei der Kategorie des Dings eins dazu. Wichtig ist das return counts am Ende des Callbacks. Ohne es wäre das Ergebnis der nächsten Runde undefined.
Aufgabe Knobelei
Schreib die Funktion createFilter(criteria). Sie bekommt ein Objekt mit den Kriterien category, query und onlyAvailable, jedes davon freiwillig, und liefert eine Funktion, die filter verwenden kann. Der Suchtext soll ohne Rücksicht auf Gross- und Kleinschreibung im Namen vorkommen.
Lösung
// Builds a test for filter from the criteria category, query and onlyAvailable, each of them optionalexport function createFilter({ category, query, onlyAvailable = false } = {}) { const wanted = query?.trim().toLowerCase() return (item) => (!category || item.category === category) && (!wanted || item.name.toLowerCase().includes(wanted)) && (!onlyAvailable || item.availableFrom === null)}Die Kriterien zerlegt die Funktion schon in der Parameterliste. Den Suchtext bereitet sie einmal vor, nicht bei jedem Ding. Die zurückgegebene Funktion prüft jedes Kriterium nur, wenn es angegeben ist, !category || lässt jedes Ding durch, solange keine Kategorie gesetzt ist. Diesen Filter brauchst du in Teil 3 wieder, wenn der Katalog in Vue ein Suchfeld bekommt.
Typische Fehler
Abschnitt betitelt „Typische Fehler“| Fehler | Was hilft |
|---|---|
map mit geschweiften Klammern liefert eine Liste voller undefined. | Im Block return schreiben oder die Klammern weglassen. |
Das Ergebnis von forEach wird weiterverwendet und ist undefined. | forEach liefert nichts. Für eine neue Liste map oder filter nehmen. |
find findet nichts, und der Zugriff auf .name wirft einen TypeError. | Das Ergebnis prüfen oder ?.name schreiben. |
reduce ohne Startwert meldet bei einer leeren Liste einen TypeError. | Den Startwert immer angeben. |
sort ändert den Katalog für alle, die ihn verwenden. | toSorted verwenden. |
Zahlen werden mit sort() als Text sortiert, 10 kommt vor 9. | Eine Vergleichsfunktion wie (a, b) => a - b übergeben. |
Ein KI-Assistent installiert eine Bibliothek wie Lodash für groupBy oder sortBy. | reduce, Object.groupBy und toSorted erledigen das ohne Paket. |
Selbstcheck
Abschnitt betitelt „Selbstcheck“Selbstcheck
- Was ist der Unterschied zwischen
mapundforEach? - Was liefert
find, wenn kein Element passt, und wasfilter? - Wozu dient das zweite Argument von
reduce? - Warum verwendet der Kurs
toSortedstattsort?
Antworten
mapliefert eine neue Liste mit den Rückgabewerten des Callbacks.forEachruft den Callback nur auf und liefertundefined.findliefertundefined,filtereine leere Liste.- Es ist der Startwert. Ohne ihn nimmt
reducedas erste Element und meldet bei einer leeren Liste einen Fehler. toSortedliefert eine sortierte Kopie und lässt das Original unverändert.sortändert das Array selbst, eine Nebenwirkung, die andere Stellen überrascht.
Auftrag
Die Auswertungen haben den Vorstand überzeugt. Martina Hostettler hat gute Nachrichten. Der Katalog liegt ab jetzt auf dem Server des Vereins, und die Freiwilligen pflegen ihn dort direkt. Die Übersicht soll ihn von dort laden. Ein Server antwortet aber nicht sofort, und manchmal gar nicht. Wie JavaScript mit solchem Warten umgeht, zeigt das nächste Kapitel.
Texte und Abbildungen stehen unter CC BY-SA 4.0, der Code unter der MIT-Lizenz und die Daten des Kurses unter CC0 1.0. Die Schrift der Abbildungen ist Kalam unter der SIL Open Font License 1.1. Die Quellen liegen auf GitHub.