Zum Inhalt
wannacode

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.

Order
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)} €`);
}
Ausgabe
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 ist return Pflicht.
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);
Ausgabe
Tom: 12.90 €
Laura: 40.00 €
Maria: 75.50 €
Gast

Eine 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());
Ausgabe
Gast
undefined

Die 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));
Ausgabe
40
45

student 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
  1. Die Pfeilfunktion ist ein Objekt vom Typ function, und student zeigt darauf. Ihr Rumpf läuft noch nicht.
  2. Eine zweite Regel, ein zweites Objekt. Keine einzige Klasse ist geschrieben.
  3. student(50) übergibt 50 an die Funktion: price ist 50, und price * 0.8 ergibt 40.
  4. 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();
Ausgabe
Maria: Kaffee
Maria: Honig
TypeError: 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 true oder false zurü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);
Ausgabe
true
-> MARIA
Gast

In 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));
Ausgabe
12.67

andThen 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));
Antwort

Sieh es dir an

Arrays nehmen Funktionen

Arrays haben Methoden, die statt einer Schleife eine Regel nehmen:
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(", ")}`));
Ausgabe
Brot
Milch
Kaffee
Berlin: Maria, Laura
Hamburg: Tom
  • filter(bedingung) macht ein neues Array nur mit den Elementen, für die die Bedingung wahr ist. Das alte ändert sich nicht: Hier ist cart let und 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 eine Map mit einem Array der Elemente für jeden Schlüssel.
  • Das forEach einer Map nimmt 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

Oft gibt eine Pfeilfunktion ihren Parameter nur an eine vorhandene Funktion weiter: (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));
Ausgabe
maria
tom
LAURA
43
[ 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);
Ausgabe
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));
Ausgabe
Anna 6
Jonas 5.5
Peter 5.5
Finn 4.75

b.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 sort aus 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);
Ausgabe
[ 1, 10, 9 ]
[ 1, 9, 10 ]
[ 12, 40, 75 ] [ 40, 12, 75 ]
  • sort sortiert das Array selbst. toSorted gibt ein neues sortiertes Array zurück, und das alte bleibt, wie es ist.

Probier es aus

Was kehrt reversed um?

Hier nimmt 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}`));
Antwort

Probier es aus

Finde den Fehler

Das Programm soll die Summe der Preise ausgeben. Es bricht schon beim ersten Preis mit einem Fehler ab. In welcher Zeile ist die Ursache?

Wähle die Zeile mit dem Fehler.

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));
Ausgabe
80
50

In Java ist das genau wegen solcher Überraschungen verboten. Muss sich die Regel einen Wert merken, übergib ihn als Parameter.

Übung

Regeln als Werte

Löse die Aufgaben in deinem Editor.

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 €.

Ausgabe
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 €.

Ausgabe
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 €
3. Rabatte nacheinander. Mach drei Rabatte als Pfeilfunktionen: einen Studentenrabatt (−10 %), einen Black-Friday-Rabatt (−20 €) und „nicht unter null“. Schreib 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?
Ausgabe
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 einzigen filter und gib den Rest mit forEach aus.
  • Schüler nach Klasse. Gruppiere Schüler nach Klasse ("10A", "10B") mit Map.groupBy und 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

1. Welche ist eine Pfeilfunktion, die die Summe von zwei Preisen zurückgibt?
2. Wie baust du aus den Funktionen big und maria die Bedingung „groß und von Maria“?
3. Was gibt ["1", "2", "3"].map(parseInt) zurück?
4. Wie sortiert students.sort((a, b) => b.grade - a.grade || a.name.localeCompare(b.name))?
5. const total = 0; prices.forEach((p) => (total += p)); Was passiert?

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