JavaScript Currying entmystifiziert: Schließfunktionen, teilweises Anwenden, Wiederverwendung
Erfahren Sie, wie Currying mehrargumentige JavaScript-Funktionen in wiederverwendbare, einargumentige Ketten umwandelt, wie sich dies von der teilweisen Anwendung unterscheidet und wann man darauf verzichten sollte.
Falls Sie immer wieder denselben ersten Argumentwert an eine Funktion übergeben, zahlen Sie bei jedem Aufruf eine kleine Gebühr für die Wiederholung. Durch Currying können Sie einen Teil der Eingabedaten einer Funktion einmal festlegen und das Ergebnis erneut verwenden. Im Folgenden erfahren Sie, was Currying ist, wie Schließungen es ermöglichen, wie es sich von der teilweisen Anwendung unterscheidet und wann es den Code tatsächlich verbessert.
Was Currying bedeutet
Currying wandelt eine Funktion, die mehrere Argumente entgegennimmt, in eine Abfolge von Funktionen um, bei denen jeweils genau ein Argument übergeben wird. Ein regulärer Aufruf liefert alle Werte auf einmal:
f(a, b, c)
Die currierte Form liefert bei jedem Aufruf einen Wert, und jeder Aufruf gibt die nächste Funktion in der Kette zurück:
f(a)(b)(c)
Jede Zwischenfunktion speichert die Werte aus früheren Aufrufen. Diese Speicherung erfolgt durch Schließungen, die weiter unten erläutert werden.
Umwandlung einer gewöhnlichen Funktion
Begonnen wird mit einer einfachen Addition mit zwei Argumenten:
function sum(a, b) {
return a + b;
}
sum(2, 3); // 5
Ein kleiner Hilfsfunktion kann jede zweiparametrige Funktion umhüllen: Die äußere Funktion nimmt den ersten Wert entgegen, gibt eine Funktion zurück, die den zweiten Wert entgegennimmt, und ruft erst anschließend die ursprüngliche Funktion mit beiden Werten auf:
function curry(fn) {
return function (a) {
return function (b) {
return fn(a, b);
};
};
}
const curriedSum = curry(sum);
curriedSum(2)(3); // 5
Was man beachten sollte:
curryist eine höherer Ordnung: Sowohl ihre Eingabe als auch ihr Ausgabe sind Funktionen.- Jede von ihr zurückgegebene Funktion erwartet nur einen Wert.
- Die innere Funktion kann weiterhin auf
azugreifen, nachdem die äußere Funktion zurückgegeben hat, weil sie dieses Argument „schließt“ (closes over it).
Dieser Hilfsfunktion sind genau zwei Parameter vorbehalten. Allgemeingültige Versionen überprüfen fn.length und sammeln weiterhin Argumente ein, bis genug vorhanden sind – doch die Idee bleibt dieselbe.
Ein praktisches Beispiel: spezialisierte Logger
Aritmetische Operationen verbergen den Nutzen, daher betrachten wir einen Logger, der ein Schweregrad-Niveau und eine Nachricht entgegennimmt:
function log(level, message) {
console.log(`[${level}] ${message}`);
}
Die Aufrufstellen beginnen schnell, dasselbe Niveau zu wiederholen:
log("ERROR", "Something broke");
log("ERROR", "Invalid token");
log("ERROR", "API failed");
Ein kurrierter Logger ermöglicht es, vorkonfigurierte Logger nur einmal zu erstellen:
function log(level) {
return function (message) {
console.log(`[${level}] ${message}`);
};
}
const errorLog = log("ERROR");
const infoLog = log("INFO");
errorLog("Something broke");
errorLog("Invalid token");
infoLog("User logged in");
Das Niveau wird festgelegt, sobald errorLog und infoLog erstellt werden. Die Aufrufstellen werden kürzer, die Absicht ist leichter lesbar, und die spezialisierten Funktionen können an anderer Stelle exportiert oder eingefügt werden. Diese Kombination aus Wiederverwendung und Ausdrucksstärke ist der Hauptgrund für die Verwendung von Kurrierung.
Teilweise Anwendung
Teilweise Anwendung bedeutet, einige Argumente einer vorhandenen Funktion im Voraus zu füllen, um eine neue Funktion zu erhalten, ohne die Struktur der ursprünglichen Funktion zu ändern. JavaScript unterstützt dies nativ über bind:
function multiply(a, b) {
return a * b;
}
const double = multiply.bind(null, 2);
double(5); // 10
Das erste bind-Argument setzt this, das von multiply ignoriert wird, weshalb der Wert null ist. multiply benötigt dennoch zwei Argumente; double ist lediglich eine Variante, bei der das erste Argument bereits vorgegeben wird.
Currying versus partielle Anwendung
Sowohl Currying als auch partielle Anwendung verfolgen dasselbe Ziel, nutzen aber unterschiedliche Mechanismen:
- Currying strukturiert eine Funktion in eine Kette von Funktionen mit einem Argument um.
- Partielle Anwendung füllt einige Argumente vorab ein und lässt die ursprüngliche Struktur der Funktion unverändert.
Jeder Schritt in einer currierten Kette ist im Grunde eine partielle Anwendung mit einem Argument, doch partielle Anwendung erfordert nicht unbedingt Currying.
Offenes Currying
Eine spielerische Variante akzeptiert weiterhin Werte, bis die Kette mit einem leeren Aufruf beendet wird:
let infiniteSum = function (a) {
return function (b) {
if (b !== undefined) return infiniteSum(a + b);
return a;
};
};
infiniteSum(1)(2)(3)(4)(); // 10
So verläuft der Ablauf:
- Jeder Aufruf mit einem Wert fügt ihn zur laufenden Summe hinzu und gibt eine neue Funktion zurück, die diese Summe enthält.
- Der letzte
()-Aufruf lässtbalsundefinedzurück, sodass die Summe ausgegeben wird. - Closures übertragen die Summe von einem Schritt zum nächsten.
Die explizite Überprüfung b !== undefined ist wichtig. Ein Wahrheitscheck wie if (b) würde 0 als Endpunkt betrachten, wodurch infiniteSum(1)(0)(2)() bei null stoppen würde anstatt 3 zurückzugeben. Dieses Muster ist eine hervorragende Übung zu Closures und ein häufiges Vorstellungsgesprächsfragen, doch der leicht übersehene letzte Aufruf macht es zu einer ungeeigneten Lösung für Produktions-APIs.
Closures sorgen dafür, dass alles funktioniert
Hier ist der kleinste kurierte Addierer:
function add(a) {
return function (b) {
return a + b;
};
}
add(3)(4); // 7
add(3) gibt die innere Funktion zurück und beendet sich, doch diese innere Funktion kann weiterhin auf a zugreifen. Das ist eine Schließung: Eine Funktion behält den Zugriff auf Variablen aus dem Scope, in dem sie definiert wurde, auch nachdem die äußere Funktion zurückgegeben hat. Da der erfasste Wert bestehen bleibt, kann die teilweise angewendete Funktion gespeichert und wiederverwendet werden:
const add3 = add(3);
add3(4); // 7
add3(10); // 13
Weitere Informationen dazu, wie diese erfassten Variablen gelöst werden, finden Sie unter wie die Scope-Kette Variablen löst.
Wann man es verwenden und wann man darauf verzichten sollte
Currying lohnt sich, wenn:
- dieselben Anfangsargumente an vielen Aufrufstellen vorkommen
- es den Code klarer statt undurchsichtiger macht
- Sie kleine, komponierbare, wiederverwendbare Funktionen möchten
Überspringen Sie dies, wenn:
- eine einfache, einmalige Funktion nichts von der Aufteilung hat
- Ihr Team mit diesem Muster nicht vertraut ist
- die aufeinanderfolgenden Aufrufe die Lesbarkeit beeinträchtigen
Zusammenfassung
Currying dreht sich im Grunde darum, die Eingaben einer Funktion nach dem Zeitpunkt ihrer Kenntnis zu ordnen: Konfigurationen wie das Protokollniveau oder die Basis-URL werden früh festgelegt, während Daten pro Aufruf später eintreffen. Closures übernehmen das Merken, bind bietet eine leichtere teilweise Anwendung, wenn man keine vollständige Kette benötigt, und die entscheidende Frage ist stets, ob der Aufrufcode leichter lesbar und wiederverwendbar wird.
Zusätzliche Literatur
- Verständnis von JavaScript-Closures und dem Event Loop — Lernen Sie, wie Closures externe Variablen speichern und wie der Event Loop den Call Stack, Microtasks sowie Macrotasks mithilfe praktischer Codebeispiele ordnet.
- Wie JavaScripts Scope Chain tatsächlich Variablen auflöst — Erklärt das Mechanismus der lexikalischen Scope Chain hinter der Variabensuche, warum „var“ zu Schleifenfehlern führt und wie Closures daraus natürlicherweise entstehen.