Eine Closure ist eine Funktion, die auf Variablen aus ihrem Definitions-Scope zugreift — auch dann noch, wenn dieser Scope längst beendet ist. Das klingt esoterisch, ist aber das Fundament unter Module-Pattern, Memoization, Higher-Order-Funktionen, React-Hooks und nahezu jedem privaten State in pre-Class JavaScript. Closures sind keine besondere Syntax — sie entstehen automatisch, wann immer eine innere Funktion eine äußere Variable referenziert. Dieser Artikel zeigt das Konzept Schritt für Schritt, die klassischen Anwendungs-Muster und die berüchtigte Schleifen-Closure-Falle, die mit var unerwartet zuschlägt.
Was ist eine Closure?
Wenn eine Funktion innerhalb einer anderen Funktion definiert wird, „sieht" sie alle Variablen der äußeren Funktion. Wird sie zurückgegeben und später aufgerufen, sieht sie diese Variablen immer noch — auch wenn die äußere Funktion längst zu Ende ist.
function erstelleZaehler() {
let stand = 0;
return function() {
stand++;
return stand;
};
}
const c1 = erstelleZaehler();
console.log(c1()); // 1
console.log(c1()); // 2
console.log(c1()); // 3
// Eigener Zähler — eigene `stand`-Variable
const c2 = erstelleZaehler();
console.log(c2()); // 1
console.log(c1()); // 4 — c1 hat seinen eigenen State1
2
3
1
4Die innere Funktion behält Zugriff auf stand, obwohl erstelleZaehler() längst returned hat. Jeder Aufruf der äußeren Funktion erzeugt einen eigenen Scope mit eigener stand-Variable — daher haben c1 und c2 unabhängige Zähler.
Lexical Scope — Definition vs. Aufruf
Closures basieren auf Lexical Scope: was eine Funktion „sieht", wird vom Ort ihrer Definition bestimmt, nicht vom Aufrufer.
const aussen = 'A';
function definiereInnen() {
const aussen = 'B'; // schattet die globale Variable
return function innen() {
console.log(aussen); // welches `aussen`? Antwort: lexikalisch B
};
}
function aufrufer() {
const aussen = 'C'; // ist hier irrelevant für innen
const fn = definiereInnen();
fn(); // → 'B' (lexikalisch), NICHT 'C' (dynamisch)
}
aufrufer();BIn Sprachen mit dynamischem Scope (etwa Klassisches Bash) wäre die Antwort 'C' — der Aufruf-Stack würde die Variable bestimmen. JavaScript ist lexikalisch — wo die Funktion steht, bestimmt, was sie sieht.
Kapselung — private State per Closure
Vor class mit #private (ES2022) waren Closures der einzige Weg, private State zu erzeugen. Das Muster: ein Object mit Methoden zurückgeben, die auf interne Closure-Variablen zugreifen.
function erstelleKonto(start) {
let saldo = start; // privat — nur durch Methoden erreichbar
return {
einzahlen(betrag) {
saldo += betrag;
return saldo;
},
abheben(betrag) {
if (betrag > saldo) throw new Error('Nicht genug');
saldo -= betrag;
return saldo;
},
stand() {
return saldo;
},
};
}
const konto = erstelleKonto(100);
console.log(konto.einzahlen(50)); // 150
console.log(konto.abheben(30)); // 120
console.log(konto.stand); // [Function] — die Methode selbst
console.log(konto.stand()); // 120
console.log(konto.saldo); // undefined — von außen unsichtbar150
120
[Function: stand]
120
undefinedsaldo existiert nur innerhalb der Closure von erstelleKonto — von außen unsichtbar, nicht änderbar. Das ist „echtes" Privat-Sein, nicht nur Konvention wie _saldo.
Module-Pattern — Closure als Modul-Ersatz
Vor ESM (ES2015) gab es kein offizielles Modul-System im Browser. Das Module-Pattern nutzte eine IIFE als Closure, um privaten State zu kapseln und nur ausgewählte Methoden zu exportieren.
const Counter = (function() {
let stand = 0; // privat
const max = 100; // privat
function inkrement() {
if (stand < max) stand++;
}
function reset() { stand = 0; }
function lese() { return stand; }
return { inkrement, reset, lese }; // public API
})();
Counter.inkrement();
Counter.inkrement();
console.log(Counter.lese()); // 2
console.log(Counter.max); // undefined — kein Zugriff2
undefinedHeute ist das durch ESM-Module ersetzt: export const inkrement = ... ist deklarativer und tooling-freundlicher. Das Muster lebt aber in viel altem Code, in CDN-Skripten und bei Library-Bundles, die für <script>-Tags ausgeliefert werden.
Memoization — Cache per Closure
Ein klassisches Closure-Pattern: eine teure Funktion in eine Wrapper-Funktion einpacken, die Ergebnisse pro Eingabe cached.
function memoize(fn) {
const cache = new Map();
return function(arg) {
if (cache.has(arg)) {
console.log(' (cache hit)');
return cache.get(arg);
}
const result = fn(arg);
cache.set(arg, result);
return result;
};
}
function teuerQuadrieren(n) {
console.log(' rechne', n);
return n * n;
}
const memoQuadrieren = memoize(teuerQuadrieren);
console.log('Aufruf 1:', memoQuadrieren(5));
console.log('Aufruf 2:', memoQuadrieren(5)); // cache hit
console.log('Aufruf 3:', memoQuadrieren(7));Aufruf 1: rechne 5
25
Aufruf 2: (cache hit)
25
Aufruf 3: rechne 7
49cache lebt in der Closure und ist von außen nicht erreichbar. Pro memoize()-Aufruf gibt es einen eigenen Cache. Das Pattern ist der Kern vieler Performance-Optimierungen (React useMemo, Vue computed, Lodash memoize).
Die Schleifen-Closure-Falle mit var
Eines der häufigsten Interview-Probleme: alle Callbacks aus einer Schleife loggen denselben Wert.
const cbs = [];
for (var i = 0; i < 3; i++) {
cbs.push(function() { console.log(i); });
}
cbs.forEach(cb => cb());
// → 3, 3, 3 — alle Closures teilen dasselbe i3
3
3var i ist function-scoped — es existiert nur eine Variable, die alle drei Closures referenzieren. Nach der Schleife ist i === 3, daher logt jede Closure 3.
const cbs = [];
for (let i = 0; i < 3; i++) {
cbs.push(function() { console.log(i); });
}
cbs.forEach(cb => cb());
// → 0, 1, 2 — jeder Durchlauf bekommt eine NEUE Bindung0
1
2let im for-Init ist Sonder-spezifiziert: pro Durchlauf eine neue Bindung. Jede Closure schließt eine eigene i-Bindung ein.
Pre-let-Workaround mit IIFE
Vor ES2015 wurde die Schleifen-Falle mit einer IIFE pro Durchlauf umgangen — jede IIFE erzeugt einen eigenen Function-Scope.
const cbs = [];
for (var i = 0; i < 3; i++) {
(function(j) {
cbs.push(function() { console.log(j); });
})(i);
}
cbs.forEach(cb => cb());
// → 0, 1, 2 — j ist pro IIFE-Aufruf eine neue Bindung0
1
2Heute nur noch historisch relevant — let löst das gleiche Problem mit weniger Syntax. Aber: in Legacy-Code begegnet dieses Muster ständig.
Closure und Mutation — geteilter State
Wenn mehrere Funktionen die gleiche Closure-Variable schließen, teilen sie den State.
function zaehlerPaar() {
let n = 0;
return {
hoch() { return ++n; },
runter() { return --n; },
wert() { return n; },
};
}
const z = zaehlerPaar();
console.log(z.hoch()); // 1
console.log(z.hoch()); // 2
console.log(z.runter()); // 1
console.log(z.wert()); // 11
2
1
1Drei Funktionen, eine geteilte Variable. Das ist gewolltes Verhalten und der Kern aller Closure-basierten Objekt-Muster.
Higher-Order-Funktionen — Closures bauen Funktionen
Funktionen, die Funktionen zurückgeben (und dabei Variablen kapseln), sind eine direkte Closure-Anwendung.
function multipliziere(faktor) {
return function(n) {
return n * faktor;
};
}
const verdoppeln = multipliziere(2);
const verdreifachen = multipliziere(3);
console.log(verdoppeln(5)); // 10
console.log(verdreifachen(5)); // 15
// Klassischer Use-Case in Array-Pipelines
const zahlen = [1, 2, 3, 4];
console.log(zahlen.map(multipliziere(10)));10
15
[ 10, 20, 30, 40 ]faktor lebt in der Closure jeder erzeugten Funktion. Das ist die Grundlage von Currying und Partial Application (eigener Artikel).
Besonderheiten
Closures sind kein Sprach-Feature — sie ENTSTEHEN automatisch
Es gibt kein closure-Keyword. Jede innere Funktion, die auf äußere Variablen zugreift, ist automatisch eine Closure. Die Sprache speichert die Scope-Chain, solange die innere Funktion irgendwo erreichbar ist. Sobald die Closure gar nicht mehr referenziert wird, kann der Garbage Collector den Scope mit aufräumen.
let in for-Init: pro Durchlauf eine neue Bindung
Sonder-Regel der Spec: for (let i = ...; ...; ...) erzeugt pro Iteration eine neue i-Bindung. Das macht Callbacks innerhalb der Schleife sicher — jeder bekommt sein eigenes i. Mit var gäbe es nur eine geteilte Bindung — der Klassiker für die 3-3-3-Falle.
Closures halten Variablen am Leben — Speicher-Aspekt
Wer eine Closure dauerhaft referenziert (z.B. als Event-Listener), hält damit auch alle Variablen im umgebenden Scope am Leben. Bei großen Objekten in unnötigen Closures verschwendet das Speicher. Sauberes Pattern: nur die wirklich gebrauchten Werte in die Closure einbinden, große Objekte explizit auf null setzen, wenn man weiß, dass sie nicht mehr gebraucht werden.
Module-Pattern war prä-ESM Standard für Privacy
Vor 2015 nutzte fast jede Library das IIFE-Module-Pattern, um privaten State zu kapseln. Heute durch ESM-Module ersetzt: const private = ...; export const public = ... erreicht dasselbe deklarativer. ESM-Module sind selbst eine Form von Closure auf File-Ebene.
React-Hooks bauen vollständig auf Closures
Jeder useState-Aufruf in einer Function-Component erzeugt eine Closure über den State-Setter. Stale-Closure-Bugs in useEffect kommen genau daher: der Effect schließt einen alten Wert ein, der nicht mehr aktuell ist. Dependency-Arrays sind der Mechanismus, der React zwingt, neue Closures zu erzeugen, wenn relevante Werte sich ändern.
WeakRef + FinalizationRegistry für weak Closures
Seit ES2021 gibt es WeakRef: eine Referenz, die den GC NICHT am Aufräumen hindert. In Kombination mit FinalizationRegistry lässt sich „weak" State in Closures bauen, der weggeräumt wird, wenn sonst niemand das Object hält. Selten gebraucht — meist nur in Cache-Implementations.
Closure auf Function-Argumente, nicht nur lokale Variablen
Auch Parameter der äußeren Funktion sind im Scope und werden von Closures referenziert. function adder(x) { return y => x + y; } — x ist der Parameter und überlebt in der zurückgegebenen Closure. Das ist das Currying-Pattern in seiner kürzesten Form.
Closures behalten die LATEST Werte, nicht Snapshots
Wenn eine Closure-Variable nach Erstellung der Closure verändert wird, sieht die Closure die NEUEN Werte. Beispiel: let i = 0; const fn = () => i; i = 5; fn(); liefert 5. Wer Snapshot-Verhalten will, muss explizit eine Kopie ziehen: const ix = i; const fn = () => ix;.
Weiterführende Ressourcen
Externe Quellen
- Closures – MDN
- Lexical scope – MDN Glossary
- Memoization – MDN Glossary
- Environment Records – ECMAScript Spec
- You Don't Know JS: Scope & Closures