Startseite / Artikel / JavaScript Currying entmystifiziert: Schließfunktionen, teilweises Anwenden, Wiederverwendung

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.

1051 Wörter

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:

  • curry ist 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 a zugreifen, 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ässt b als undefined zurü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
  • Man erstellt Hilfsfunktionen wie Logger, Validatoren, Formater oder API-Hilfsmittel, bei denen der Wert einer Konfiguration bereits früh bekannt ist.
  • Ü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

  • Aufrüsten auf htmx 4: Fetch, explizite Vererbung und was bricht — Erkennen Sie die Architekturänderungen bei htmx 4, von dem auf Fetch basierenden Kern und der expliziten Attributvererbung über Fehlerbehandlung, Morphing und Historie bis hin zur Planung einer sicheren Migration.