JavaScript-Patterns, die geblieben sind: Module, Higher-Order Functions, Currying
Drei JavaScript-Muster haben jeden Framework-Wechsel überlebt: das Module-Pattern für saubere Kapselung, Higher-Order Functions für Funktionen als Werte und Currying für spezialisierte Varianten. Warum sie das Fundament moderner Utility-Libraries bilden.
In meinen Schulungen kommt fast immer dieselbe Frage: „Welches Framework sollen wir lernen?" Meine Antwort enttäuscht die Teilnehmer regelmäßig, weil sie mit dem Framework nichts zu tun hat. Ich sage: Lernt die Muster, die unter den Frameworks liegen. Die Bibliotheken kommen und gehen – ich habe in wenigen Jahren jQuery, Backbone, Angular 1, dann Angular 2 und React vorbeiziehen sehen. Was blieb, war ein kleiner Kern an JavaScript-Mustern, den man in jedem dieser Projekte wiederfindet, sobald man genau hinschaut.
Drei davon greife ich hier heraus, weil sie sich in jeder Codebase auszahlen und weil ich sie in jedem Kurs wieder erklären darf: das Module-Pattern, Higher-Order Functions und Currying beziehungsweise Partial Application. Sie bauen aufeinander auf, und wer sie verstanden hat, liest modernen JavaScript-Code plötzlich mit anderen Augen. Ich setze dabei voraus, dass ES6 (also ES2015) als Baseline zur Verfügung steht – wer die Grundlagen dazu auffrischen will, findet sie in HTML5, CSS3 und ECMAScript als Baseline.
Die drei Muster in einem Satz:
- Module-Pattern kapselt Zustand und gibt nur ein bewusst gewähltes öffentliches API frei.
- Higher-Order Functions behandeln Funktionen als Werte – sie nehmen Funktionen entgegen oder geben welche zurück.
- Currying und Partial Application erzeugen aus einer generischen Funktion spezialisierte, vorkonfigurierte Varianten.
Module-Pattern: zwei Wege, ein Ziel
Das älteste Problem in Browser-JavaScript ist der globale Namespace. Früher landete jede Variable, die man nicht bewusst kapselte, am window-Objekt, und zwei Skripte konnten sich gegenseitig überschreiben, ohne es zu merken. Die Lösung vor ES6 war eine Konvention, kein Sprachmittel: die Immediately Invoked Function Expression, kurz IIFE. Man packt Code in eine Funktion, ruft sie sofort auf und nutzt den dabei entstehenden Scope als privaten Raum. Was drinnen deklariert wird, bleibt drinnen – außer man gibt es bewusst als Objekt zurück. Diese Rückgabe des öffentlichen APIs nennt man auch Revealing Module Pattern.
const counterModule = (function () {
let count = 0; // private, not accessible from outside
return {
increment() {
return ++count;
},
get() {
return count;
}
};
})();
counterModule.increment(); // 1
counterModule.increment(); // 2
counterModule.get(); // 2
counterModule.count; // undefined – deliberately not exposed
Der Trick liegt in der Closure: Die zurückgegebenen Methoden behalten Zugriff auf count, obwohl die äußere Funktion längst durchgelaufen ist. Von außen gibt es keinen Weg, count direkt zu setzen. Genau das wollten wir – Kapselung ohne Sprachunterstützung, allein durch Scope und Closure.
Seit ES2015 gibt es echte Module in der Sprache. Jede Datei ist ihr eigener Modul-Scope, nichts leakt automatisch nach global, und was nach außen sichtbar sein soll, markiert man mit export. Der Import erfolgt statisch mit import. Dasselbe Beispiel als ES-Modul:
// counter.js
let count = 0; // module-private, only visible in this file
export function increment() {
return ++count;
}
export function get() {
return count;
}
// app.js
import { increment, get } from './counter.js';
increment(); // 1
increment(); // 2
get(); // 2
Semantisch machen beide dasselbe: privater Zustand, ein klar definiertes öffentliches API. Der Unterschied ist die Ebene. Beim IIFE ist die Kapselung eine Laufzeit-Konstruktion, die auf dem Verhalten von Funktionen und Closures beruht. Beim ES-Modul ist sie sprachlich garantiert und statisch – Werkzeuge können die Imports schon vor der Ausführung analysieren, ungenutzten Code entfernen und Zyklen erkennen. Die Bindings sind außerdem live und read-only: Der Importeur sieht Änderungen am exportierten Wert, kann ihn aber nicht von außen überschreiben. Und Module laufen implizit im strict mode.
graph LR A[IIFE + Closure] --> C[Kapselung<br/>privater Zustand<br/>klares öffentliches API] B[ES6 import/export] --> C
Ein ehrliches Wort zum Stand 2017: Node.js kennt native ES-Module noch nicht stabil. Dort schreibt man weiterhin CommonJS mit require und module.exports; das experimentelle --experimental-modules-Flag ist gerade erst mit Node 8.5.0 erschienen. In den Browsern ist die Unterstützung frisch – Chrome 61 und Safari 10.1 können type="module" bereits, Firefox und Edge noch hinter einem Flag. In der Praxis bündeln wir 2017 also mit webpack oder Rollup und lassen Babel die import/export-Syntax übersetzen. Das ändert am Muster nichts: Wir denken in Modulen, auch wenn das Deployment noch über einen Bundler läuft.
Higher-Order Functions: Funktionen sind Werte
Der zweite Baustein ist die eigentliche Stärke von JavaScript, und die meisten Teilnehmer haben sie benutzt, lange bevor sie einen Namen dafür hatten. In JavaScript sind Funktionen First-Class Values: Man kann sie in Variablen speichern, als Argument übergeben und aus anderen Funktionen zurückgeben. Eine Funktion, die eine Funktion als Argument entgegennimmt oder eine Funktion zurückgibt, heißt Higher-Order Function.
Die bekanntesten Beispiele sind die Array-Methoden map, filter und reduce. Sie sind seit ES5 (2009) überall verfügbar und ersetzen imperative Schleifen durch deklarative Transformationen. Statt zu beschreiben, wie iteriert wird, beschreibt man, was mit jedem Element geschieht:
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
Jede dieser Methoden ist selbst eine Higher-Order Function, weil sie einen Callback entgegennimmt. map ruft den Callback für jedes Element auf und sammelt die Rückgaben in einem neuen Array. filter behält nur die Elemente, für die der Callback wahr liefert. reduce faltet das Array auf einen einzigen Wert zusammen, indem es einen Akkumulator durch alle Elemente trägt.
Die andere Hälfte des Musters – Funktionen, die Funktionen zurückgeben – ist der Schlüssel zu allem, was danach kommt. Eine Funktion, die eine konfigurierte Funktion produziert, nennt man manchmal Funktionsfabrik:
const multiplyBy = factor => n => n * factor;
const double = multiplyBy(2);
const triple = multiplyBy(3);
double(5); // 10
triple(5); // 15
[1, 2, 3].map(double); // [2, 4, 6]
multiplyBy(2) gibt keine Zahl zurück, sondern eine neue Funktion, die sich factor per Closure gemerkt hat. double und triple sind spezialisierte Werkzeuge, gebaut aus einer einzigen generischen Funktion. Und weil sie ganz normale Funktionen sind, lassen sie sich direkt an map übergeben. Genau hier verzahnen sich Higher-Order Functions mit dem nächsten Muster.
Currying und Partial Application: aus generisch wird speziell
Currying bedeutet, eine Funktion mit mehreren Argumenten in eine Kette einstelliger Funktionen umzuwandeln. Aus f(a, b, c) wird f(a)(b)(c). Jeder Aufruf nimmt genau ein Argument und gibt eine Funktion zurück, die auf das nächste wartet – bis alle Argumente da sind und das Ergebnis berechnet wird. Realisiert wird das wieder über Closures.
const add = (a, b) => a + b; // ordinary, two arguments at once
const curriedAdd = a => b => a + b; // curried, unary chain
curriedAdd(10)(5); // 15
const add10 = curriedAdd(10); // specialized function, waits for b
add10(5); // 15
add10(90); // 100
Eng verwandt, aber nicht dasselbe ist Partial Application. Dabei fixiert man einige Argumente einer Funktion und erhält eine neue Funktion mit weniger Parametern. Der Unterschied zum Currying: Partial Application zwingt nicht in einstellige Ketten, sondern legt einfach ein paar Argumente vorab fest. In JavaScript kann man das ganz ohne eigene Helfer mit Function.prototype.bind bekommen, das ebenfalls seit ES5 verfügbar ist:
const add = (a, b) => a + b;
// bind fixes this (here null) and the leading arguments
const add10 = add.bind(null, 10);
add10(5); // 15
add10(90); // 100
Der Nutzen zeigt sich, sobald man solche spezialisierten Funktionen komponiert. Aus einem kleinen Vorrat generischer Bausteine baut man passgenaue Varianten und steckt sie zusammen. Ein Beispiel, das zeigt, wie natürlich sich so ein Baustein in die Array-Methoden fügt – add10 aus dem bind-Schritt wandert direkt in map:
const numbers = [1, 2, 3, 4];
const shifted = numbers.map(add10); // [11, 12, 13, 14]
const total = shifted.reduce((sum, n) => sum + n, 0); // 50
Der Bogen von der generischen zur spezialisierten Funktion lässt sich gut als Fluss lesen:
graph LR A["add(a, b)<br/>generisch, zwei Argumente"] --> B["curriedAdd(10)<br/>bzw. add.bind(null, 10)"] B --> C["add10(n)<br/>spezialisiert, ein Argument"]
Und genau das ist der Grund, warum diese Muster über Jahre geblieben sind: Sie sind das Fundament moderner Utility-Libraries. Ramda (seit 2013) und der functional-programming-Zweig von lodash liefern ihre Funktionen von Haus aus curried und mit „data-last"-Signatur aus – die Daten kommen zuletzt, damit man die Konfiguration vorab fixieren und die entstehenden Bausteine bequem komponieren kann. Wer das Prinzip hinter multiplyBy und curriedAdd verstanden hat, versteht auch, warum diese Bibliotheken so aufgebaut sind.
Drei Fallstricke, die ich immer wieder sehe
So elegant die Muster sind, so zuverlässig stolpern Teilnehmer über dieselben Stellen. Drei davon will ich benennen, weil sie in echten Codebases teuer werden.
Der erste betrifft die this-Bindung bei Higher-Order Functions. Übergibt man eine Objekt-Methode direkt als Callback, geht der Receiver verloren. Die Methode wird ohne ihr Objekt aufgerufen, und this ist dann undefined oder das globale Objekt:
const counter = {
count: 0,
increment() {
this.count += 1; // this depends on HOW it is called
}
};
[1, 2, 3].forEach(counter.increment);
// TypeError, i.e. counter.count stays 0 – this is not counter
Die Lösung 2017 ist eindeutig. Man bindet this mit bind fest, umschließt den Aufruf mit einer Arrow Function, die ihr this lexikalisch aus der Umgebung bezieht, oder nutzt den thisArg-Parameter, den map, filter und forEach als zweites Argument anbieten:
[1, 2, 3].forEach(counter.increment.bind(counter));
[1, 2, 3].forEach(() => counter.increment());
[1, 2, 3].forEach(counter.increment, counter); // thisArg
Arrow Functions sind hier das schärfste Werkzeug, weil sie gar kein eigenes this mitbringen – sie erben es aus dem umgebenden Scope. Damit verschwindet die Fehlerklasse in vielen Fällen ganz.
Der zweite Fallstrick ist die Überverwendung von Currying. Tief verschachtelte f(a)(b)(c)(d)-Ketten sehen im Blogpost schick aus, aber im Team-Code kosten sie Lesbarkeit. Sie sind schwerer zu debuggen, die Stacktraces werden unübersichtlich, und wer den Code sechs Monate später liest, muss die Kette gedanklich wieder auffalten. Meine Faustregel: Currying dort einsetzen, wo Spezialisierung oder Komposition einen echten Vorteil bringt – nicht als Selbstzweck, weil es funktional wirkt.
Der dritte betrifft reduce ohne Startwert. Lässt man das initialValue-Argument weg, nimmt reduce das erste Array-Element als Startwert – und wirft bei einem leeren Array einen TypeError. Außerdem ist die Semantik des Akkumulators dann an den Elementtyp gekoppelt, was schnell zu subtilen Fehlern führt:
[].reduce((sum, n) => sum + n); // TypeError: Reduce of empty array
[].reduce((sum, n) => sum + n, 0); // 0 – correct, because initial value set
Ich setze den Startwert deshalb grundsätzlich. Es kostet nichts und macht die Absicht explizit: Der Typ des Akkumulators steht dann sofort da.
Fazit
Wenn ich die drei Muster nebeneinanderlege, fällt eine gemeinsame Linie auf. Das Module-Pattern kapselt Zustand, egal ob per IIFE und Closure oder per ES-Modul – das Ziel bleibt dasselbe, nur die Ebene wandert von der Konvention in die Sprache. Higher-Order Functions machen Funktionen zu Werten und ersetzen Schleifen durch Transformationen. Currying und Partial Application bauen aus generischen Funktionen spezialisierte Varianten, die sich komponieren lassen. Die Syntax hat sich zwischen ES5 und ES6 verschoben, das Prinzip dahinter nicht.
Genau das ist die Lehre, die ich meinen Teilnehmern mitgebe: Muster schlagen Syntax. Die Sprach-Features werden sich weiter ändern – zwischen der ersten IIFE und dem heutigen import liegen Jahre –, aber Kapselung, Funktionen als Werte und Spezialisierung durch Vorkonfiguration sind stabil geblieben. Wer diesen Kern beherrscht, muss beim nächsten Framework-Wechsel nicht von vorn anfangen. Er erkennt die alten Bekannten in neuem Gewand wieder, und das ist auf lange Sicht mehr wert als jede aktuelle Bibliotheksversion.
Kommentare