Pfeilfunktionen
Kurz gesagt: Eine Pfeilfunktion ist eine kurze Schreibweise für eine Funktion, die du als Wert übergibst, zum Beispiel zum Filtern oder Sortieren.
Problem
Eine ganze Funktion für einen Vergleich
Die Bestellungen sollen nach Summe sortiert werden.
class Order { constructor(customer, total) { this.customer = customer; this.total = total; }}const orders = [ new Order("Maria", 75.5), new Order("Tom", 12.9), new Order("Laura", 40),];
function byTotal(a, b) { return a.total - b.total;}
orders.sort(byTotal);
for (const o of orders) { console.log(`${o.customer}: ${o.total.toFixed(2)} €`);}Tom: 12.90 €Laura: 40.00 €Maria: 75.50 €sort nimmt eine Vergleichsfunktion. Sie bekommt zwei Bestellungen und gibt eine negative Zahl zurück, wenn die erste vor die zweite gehört, eine positive, wenn sie danach kommt, und 0, wenn beide gleich sind. a.total - b.total macht genau das.
Es funktioniert. Aber für einen Vergleich schreibst du eine ganze Funktion mit Namen, weit weg von der Stelle, an der du sie benutzt. Und sie wird nur hier aufgerufen. Du willst nur die Regel übergeben.
Die Idee
Eine Pfeilfunktion: nur die Regel
Dieselbe Sortierung in einer Zeile:
orders.sort((a, b) => a.total - b.total);(a, b) => a.total - b.total heißt Pfeilfunktion (englisch arrow function). In anderen Sprachen heißt dasselbe Lambda. Links von => stehen die Parameter, rechts der Rumpf. Das ist die Funktion byTotal ohne Namen, ohne function und ohne return.
Die Schreibweisen:
- Ein einzelner Parameter darf ohne Klammern stehen:
o => o.total > 50. Meist schreibt man die Klammern:(o) => .... Ohne Parameter ist es() => .... - Ein Rumpf mit mehreren Zeilen steht in
{ }, und dann istreturnPflicht.
const byTotal = (a, b) => a.total - b.total;const byTotalLong = (a, b) => { const diff = a.total - b.total; return diff < 0 ? -1 : diff > 0 ? 1 : 0;};const guest = () => new Order("Gast", 0);
orders.sort(byTotalLong);for (const o of orders) { console.log(`${o.customer}: ${o.total.toFixed(2)} €`);}console.log(guest().customer);Tom: 12.90 €Laura: 40.00 €Maria: 75.50 €GastEine Pfeilfunktion ist ein Wert: Du kannst sie in eine Variable legen und später übergeben.
Genauer, für Neugierige
Ist der Rumpf ein Objekt, steht er in runden Klammern. Ohne sie wird { als Beginn eines Rumpfs gelesen, und die Funktion gibt undefined zurück:
const guest = () => ({ customer: "Gast" });const broken = () => { customer: "Gast" };console.log(guest().customer);console.log(broken());GastundefinedDie Idee
Eine Funktion ist ein Wert
Woher weiß sort, dass (a, b) => ... die Vergleichsfunktion ist? Mehr muss es nicht wissen. In JavaScript ist eine Funktion ein Wert, wie eine Zahl oder ein String: Du kannst sie in eine Variable legen, an eine andere Funktion übergeben und mit Klammern aufrufen.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));4045student ist ein Objekt vom Typ function: typeof student ist "function". Du rufst es wie jede Funktion auf: student(50). Zwei Preisregeln und keine einzige Klasse.
Java braucht hier ein funktionales Interface: ein Interface mit einer Methode, das sagt, welche Art von Regel es ist. JavaScript schreibt keine Typen und hat keine Interfaces, deshalb braucht es so etwas nicht. Jede Funktion kann übergeben und aufgerufen werden.
const student = (price) => price * 0.8;const minusFive = (price) => price - 5;
console.log(student(50));console.log(minusFive(50));Schritt für Schritt, in Worten
- Die Pfeilfunktion ist ein Objekt vom Typ function, und student zeigt darauf. Ihr Rumpf läuft noch nicht.
- Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
- student(50) übergibt 50 an die Funktion: price ist 50, und price * 0.8 ergibt 40.
- minusFive(50): dieselbe Zahl, eine andere Regel, 45.
Genauer, für Neugierige
Eine Pfeilfunktion nimmt this von der Stelle, an der sie geschrieben ist, wie ein Lambda in Java. Eine Funktion, die mit function geschrieben ist, hat ihr eigenes this, und wenn forEach sie aufruft, ist das undefined:
class Cart { constructor(owner) { this.owner = owner; this.items = ["Kaffee", "Honig"]; }
print() { this.items.forEach((item) => console.log(`${this.owner}: ${item}`)); }
printOld() { this.items.forEach(function (item) { console.log(`${this.owner}: ${item}`); }); }}
const cart = new Cart("Maria");cart.print();cart.printOld();Maria: KaffeeMaria: HonigTypeError: Cannot read properties of undefined (reading 'owner')Schreib eine Regel in einer Methode einer Klasse deshalb als Pfeilfunktion.
Die Idee
Die vier Arten von Regeln
Der Typ jeder Regel ist function, aber es gibt vier Arten von Regeln. Es hilft, sie zu erkennen, weil die Funktionen, die Regeln annehmen, eine bestimmte Art wollen:
- Bedingung: nimmt einen Wert, gibt
trueoderfalsezurück. „Ist die Bestellung groß?“ - Umwandlung: nimmt eine Sache, gibt eine andere zurück. Aus einer Bestellung ein Etikett.
- Aktion: nimmt einen Wert und tut etwas damit: gibt ihn aus, speichert ihn. Ihr Ergebnis wird nicht benutzt.
- Lieferant: nimmt nichts, gibt ein neues Objekt zurück, wenn du fragst.
const big = (o) => o.total >= 50;const label = (o) => o.customer.toUpperCase();const show = (text) => console.log("-> " + text);const guest = () => new Order("Gast", 0);
const order = new Order("Maria", 75.5);console.log(big(order));show(label(order));console.log(guest().customer);true-> MARIAGastIn Java sind Regeln Objekte mit Methoden, die sie zusammensetzen: Bedingungen verbindet man mit and, or und negate („nicht“), und Funktionen reiht man mit andThen hintereinander. JavaScript hat solche Methoden nicht. Die neue Regel ist eine neue Funktion, die die alten aufruft: (o) => big(o) && maria(o). Zwei Funktionen hintereinander setzt man auch mit einer Funktion zusammen:
const withVat = (price) => price * 1.2;const rounded = (price) => Math.round(price * 100) / 100;const andThen = (f, g) => (x) => g(f(x));
const finalPrice = andThen(withVat, rounded);console.log(finalPrice(10.555));12.67andThen nimmt zwei Funktionen und gibt eine dritte zurück: Sie ruft die erste auf und übergibt deren Ergebnis an die zweite.
Genauer, für Neugierige
Eine Aktion gibt trotzdem etwas zurück: undefined, weil das das Ergebnis von console.log ist. console.log(show("Kaffee")) gibt „-> Kaffee“ aus und dann undefined.
Probier es aus
Zusammengesetzte Bedingungen
Was gibt das Programm aus?
const big = (o) => o.total >= 50;const maria = (o) => o.customer === "Maria";const bigAndMaria = (o) => big(o) && maria(o);const bigOrMaria = (o) => big(o) || maria(o);const notBig = (o) => !big(o);
const small = new Order("Maria", 30);const large = new Order("Tom", 80);
console.log(bigAndMaria(small));console.log(bigOrMaria(small));console.log(notBig(large));bigAndMaria false, und bigOrMaria ergibt true, weil sie von Maria ist. Toms Bestellung hat 80 €, sie ist groß, und ! dreht die Antwort um: false.Sieh es dir an
Arrays nehmen Funktionen
let cart = ["Brot", "", "Milch", " ", "Kaffee"];cart = cart.filter((item) => item.trim() !== "");cart.forEach((item) => console.log(item));
const customers = [ { name: "Maria", city: "Berlin" }, { name: "Tom", city: "Hamburg" }, { name: "Laura", city: "Berlin" },];const byCity = Map.groupBy(customers, (c) => c.city);byCity.forEach((list, city) => console.log(`${city}: ${list.map((c) => c.name).join(", ")}`));BrotMilchKaffeeBerlin: Maria, LauraHamburg: Tomfilter(bedingung)macht ein neues Array nur mit den Elementen, für die die Bedingung wahr ist. Das alte ändert sich nicht: Hier istcartletund zeigt einfach auf das neue. Eine Methode, die Elemente nach einer Bedingung aus dem Array selbst entfernt, gibt es nicht.forEach(aktion)führt die Aktion mit jedem Element aus.Map.groupBy(array, funktion)gruppiert: Die Funktion sagt den Schlüssel jedes Elements, und das Ergebnis ist eineMapmit einem Array der Elemente für jeden Schlüssel.- Das
forEacheinerMapnimmt eine Funktion mit zwei Parametern, aber in umgekehrter Reihenfolge: zuerst den Wert, dann den Schlüssel.
{ name: "Maria", city: "Berlin" } ist ein Objekt ohne Klasse: nur Felder. Eine Map hält die Schlüssel in der Reihenfolge, in der sie hinzugefügt wurden, deshalb kommt Berlin vor Hamburg.
Die Idee
Eine Funktion über ihren Namen, ohne Klammern
(s) => Number(s). Dann kannst du die Funktion selbst übergeben, über ihren Namen und ohne Klammern:const names = ["maria", "tom"];names.forEach((name) => console.log(name));
const upper = (s) => s.toUpperCase();const parse = Number;console.log(upper("laura"));console.log(parse("42") + 1);
console.log(["1", "2", "3"].map(Number));console.log(["1", "2", "3"].map(parseInt));mariatomLAURA43[ 1, 2, 3 ][ 1, NaN, NaN ]Java hat dafür eine eigene Schreibweise, die Methodenreferenz mit ::. In JavaScript reicht der Name, mit einem Haken: Die Funktion bekommt alles, was der Aufrufer ihr übergibt. map und forEach übergeben das Element, seinen Index und das ganze Array. Number schaut nur auf das erste und funktioniert. parseInt nimmt einen zweiten Parameter, die Basis des Zahlensystems, und bekommt den Index des Elements: parseInt("2", 1) ist NaN. Dasselbe passiert mit console.log, das alles ausgibt, was es bekommt:
["maria", "tom"].forEach(console.log);maria 0 [ 'maria', 'tom' ]tom 1 [ 'maria', 'tom' ]Eine Methode des Parameters, wie toUpperCase, hat keine Kurzform. String.prototype.toUpperCase kann man nicht direkt übergeben: Das Programm bricht mit einem TypeError ab. Du schreibst (s) => s.toUpperCase().
Übergib also nur Funktionen über ihren Namen, deren Parameter du kennst. Sonst schreib eine Pfeilfunktion.
Sieh es dir an
Sortieren nach mehreren Feldern
Die Vergleichsfunktion kann mehrere Felder nacheinander prüfen. Ergibt der erste Vergleich 0, geht || zum nächsten weiter: 0 ist in JavaScript „falsy“.
Die Schüler werden nach Note sortiert, von der höchsten an, und bei gleicher Note nach Name:
const students = [ new Student("Jonas", 5.5), new Student("Anna", 6), new Student("Peter", 5.5), new Student("Finn", 4.75),];students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name));Anna 6Jonas 5.5Peter 5.5Finn 4.75b.grade - a.grade stellt die höhere Note nach vorn. Bei gleicher Note vergleicht a.name.localeCompare(b.name) die Namen alphabetisch. Du liest es von links nach rechts: „nach Note umgekehrt, und bei Gleichheit nach Name“.
Genauer, für Neugierige
- Ohne Funktion macht
sortaus den Elementen Strings und vergleicht die Strings. Bei Zahlen ergibt das die falsche Reihenfolge:
console.log([10, 9, 1].sort());console.log([10, 9, 1].sort((a, b) => a - b));
const prices = [40, 12, 75];const sorted = prices.toSorted((a, b) => a - b);console.log(sorted, prices);[ 1, 10, 9 ][ 1, 9, 10 ][ 12, 40, 75 ] [ 40, 12, 75 ]sortsortiert das Array selbst.toSortedgibt ein neues sortiertes Array zurück, und das alte bleibt, wie es ist.
Probier es aus
Was kehrt reversed um?
reversed eine Vergleichsfunktion und gibt eine neue zurück, die a und b vertauscht.Was gibt das Programm aus?
const students = [ new Student("Jonas", 5.5), new Student("Anna", 6), new Student("Peter", 5.5), new Student("Finn", 4.75),];
const byGradeThenName = (a, b) => a.grade - b.grade || a.name.localeCompare(b.name);const reversed = (compare) => (a, b) => compare(b, a);
students.sort(reversed(byGradeThenName));students.forEach((s) => console.log(`${s.name} ${s.grade}`));reversed kehrt die ganze Funktion um, die es bekommt, und die ist „nach Note, dann nach Name“. Also werden beide umgekehrt: Die Noten gehen von der höchsten an, und Peter kommt vor Jonas. Willst du nur die Noten umgekehrt, vertauschst du a und b nur im Vergleich der Noten, wie auf der Karte davor.Probier es aus
Finde den Fehler
Wähle die Zeile mit dem Fehler.
total ist const, und die Pfeilfunktion versucht, es zu ändern:
TypeError: Assignment to constant variable.Der Fehler zeigt sich in Zeile 3, aber die Ursache ist in Zeile 2: Der Wert eines const ändert sich nicht. Mit let total = 0; gibt das Programm 127 aus.
Hier unterscheidet sich JavaScript von Java. Dort bekommt das Lambda eine Kopie des Werts und kann die lokale Variable gar nicht ändern. In JavaScript sieht die Pfeilfunktion die Variable selbst: Sie kann sie lesen und, wenn sie let ist, ändern. Für eine Summe gibt es aber einen klareren Weg, reduce, in der nächsten Lektion.
Genauer, für Neugierige
Eine Pfeilfunktion sieht die Variable selbst, keine Kopie. Ändert sich die Variable nach der Pfeilfunktion, sieht der nächste Aufruf den neuen Wert:
let rate = 0.2;const discount = (price) => price * (1 - rate);console.log(discount(100));
rate = 0.5;console.log(discount(100));8050In Java ist das genau wegen solcher Überraschungen verboten. Muss sich die Regel einen Wert merken, übergib ihn als Parameter.
Übung
Regeln als Werte
1. Sortieren nach mehreren Feldern. Eine Klasse Product mit Name, Kategorie und Preis. Sortiere die Produkte nach Kategorie, innerhalb einer Kategorie vom teuersten an und bei gleichem Preis nach Name, mit einem sort und einer Vergleichsfunktion mit ||. Die Produkte sind: Maus, Zubehör, 25 €; Laptop, Computer, 1299 €; Tastatur, Zubehör, 49,90 €; Tablet, Computer, 399 €; Kopfhörer, Zubehör, 49,90 €.
Computer | Laptop | 1299.00 €Computer | Tablet | 399.00 €Zubehör | Kopfhörer | 49.90 €Zubehör | Tastatur | 49.90 €Zubehör | Maus | 25.00 €2. Bestellungen filtern. Gib Order eine Stadt. Schreib eine Funktion select(orders, rule), die eine neue Liste nur mit den Bestellungen zurückgibt, für die die Regel wahr ist. Mach zwei Regeln, hamburg und big (mindestens 50 €), und gib die Bestellungen aus, für die beide wahr sind, die außerhalb von Hamburg und die „in Hamburg oder mindestens 100 €“. Jede der drei Regeln ist eine Pfeilfunktion, die hamburg und big aufruft. Die Bestellungen: Maria, Hamburg, 75,50 €; Tom, Dresden, 12,90 €; Laura, Hamburg, 40 €; Lukas, Berlin, 120 €.
Hamburg, ab 50 €: Maria, Hamburg: 75.50 €Außerhalb von Hamburg: Tom, Dresden: 12.90 € Lukas, Berlin: 120.00 €Kostenlose Lieferung (Hamburg oder ab 100 €): Maria, Hamburg: 75.50 € Laura, Hamburg: 40.00 € Lukas, Berlin: 120.00 €andThen(f, g), das eine Funktion zurückgibt: zuerst f, dann g. Verkette damit die Rabatte in zwei Reihenfolgen: zuerst den Studentenrabatt und zuerst den Freitagsrabatt. Probier es mit 120 € und mit 15 €. Spielt die Reihenfolge eine Rolle?120.00 € -> Student, dann Freitag: 88.00 €; Freitag, dann Student: 90.00 €15.00 € -> Student, dann Freitag: 0.00 €; Freitag, dann Student: 0.00 €Weitere Aufgaben
- Eine Liste aufräumen. Entferne aus einem Array von E-Mail-Adressen die leeren und die ohne
@mit einem einzigenfilterund gib den Rest mitforEachaus. - Schüler nach Klasse. Gruppiere Schüler nach Klasse (
"10A","10B") mitMap.groupByund gib jede Klasse mit den Namen darin aus. - Validatoren. Mach ein Objekt mit drei Validatoren als Pfeilfunktionen: nicht leerer Text, nur Ziffern (
/^\d+$/.test(value)), Länge bis 10. Probier jeden an ein paar Strings aus.
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Interfaces ohne interface · Arrays: push, splice und sort · Map und Set · Klassen und Daten in Klassen
- Geht weiter in: Array-Methoden · Daten, switch und Destrukturierung · Entwurfsmuster · Tests mit node:test
- Aufgaben zum Thema: Modernes JavaScript