Closures & Scope in JavaScript
Closures & Scope in JavaScript
Closures sind eines der mächtigsten und am häufigsten missverstandenen Konzepte in JavaScript. Wenn du Closures verstehst, verstehst du JavaScript auf einem tieferen Level.
Was ist Scope?
Scope (Gültigkeitsbereich) bestimmt, wo Variablen sichtbar und zugänglich sind.
Globaler Scope
// Globale Variable - überall sichtbar
const globaleVar = "Ich bin global";
function test() {
console.log(globaleVar); // ✅ Zugriff möglich
}
test(); // "Ich bin global"
console.log(globaleVar); // "Ich bin global"
Funktions-Scope
function beispiel() {
const lokaleVar = "Ich bin lokal";
console.log(lokaleVar); // ✅ Zugriff möglich
}
beispiel();
console.log(lokaleVar); // ❌ ReferenceError: lokaleVar is not defined
Block-Scope (let/const)
if (true) {
const blockVar = "Ich bin im Block";
let auchImBlock = "Auch hier";
var nichtImBlock = "Ich entkomme!"; // var ignoriert Block-Scope!
}
console.log(blockVar); // ❌ ReferenceError
console.log(auchImBlock); // ❌ ReferenceError
console.log(nichtImBlock); // ✅ "Ich entkomme!" (var!)
Scope Chain (Scope-Kette)
JavaScript sucht Variablen von innen nach außen:
const aussen = "außen";
function aeussere() {
const mitte = "mitte";
function innere() {
const innen = "innen";
// Zugriff auf alle äußeren Scopes
console.log(innen); // "innen"
console.log(mitte); // "mitte"
console.log(aussen); // "außen"
}
innere();
console.log(innen); // ❌ ReferenceError
}
aeussere();
Was ist eine Closure?
Eine Closure ist eine Funktion, die sich an ihre lexikalische Umgebung (den Scope, in dem sie definiert wurde) “erinnert” - auch nachdem die äußere Funktion beendet ist.
Das einfachste Beispiel
function erstelleZaehler() {
let count = 0; // Diese Variable "lebt weiter"
return function() {
count++; // Zugriff auf count - obwohl erstelleZaehler() beendet ist!
return count;
};
}
const zaehler = erstelleZaehler();
console.log(zaehler()); // 1
console.log(zaehler()); // 2
console.log(zaehler()); // 3
// count ist von außen nicht zugänglich!
// console.log(count); // ReferenceError
Was passiert hier?
erstelleZaehler()wird aufgerufencountwird mit 0 initialisiert- Eine innere Funktion wird zurückgegeben
erstelleZaehler()ist fertig - abercountlebt weiter!- Die innere Funktion “schließt über” (closes over)
count - Jeder Aufruf von
zaehler()hat Zugriff auf dasselbecount
Mehrere unabhängige Closures
function erstelleZaehler() {
let count = 0;
return function() {
count++;
return count;
};
}
// Zwei separate Zähler mit eigenen count-Variablen!
const zaehlerA = erstelleZaehler();
const zaehlerB = erstelleZaehler();
console.log(zaehlerA()); // 1
console.log(zaehlerA()); // 2
console.log(zaehlerA()); // 3
console.log(zaehlerB()); // 1 (unabhängig!)
console.log(zaehlerB()); // 2
Praktische Anwendungen
1. Private Variablen
JavaScript hat keine echten privaten Variablen, aber Closures können das simulieren:
function erstelleBankKonto(startGuthaben) {
let guthaben = startGuthaben; // "Privat"
return {
einzahlen: function(betrag) {
if (betrag > 0) {
guthaben += betrag;
return guthaben;
}
},
abheben: function(betrag) {
if (betrag > 0 && betrag <= guthaben) {
guthaben -= betrag;
return betrag;
}
return 0;
},
getGuthaben: function() {
return guthaben;
}
};
}
const konto = erstelleBankKonto(1000);
console.log(konto.getGuthaben()); // 1000
konto.einzahlen(500);
console.log(konto.getGuthaben()); // 1500
konto.abheben(200);
console.log(konto.getGuthaben()); // 1300
// Direkter Zugriff unmöglich!
console.log(konto.guthaben); // undefined
2. Factory Functions
function erstelleMultiplikator(faktor) {
return function(zahl) {
return zahl * faktor;
};
}
const verdopple = erstelleMultiplikator(2);
const verdreifache = erstelleMultiplikator(3);
const verzehnfache = erstelleMultiplikator(10);
console.log(verdopple(5)); // 10
console.log(verdreifache(5)); // 15
console.log(verzehnfache(5)); // 50
3. Event Handler mit Daten
function erstelleButtonHandler(name) {
return function() {
console.log(`Button ${name} wurde geklickt!`);
};
}
const buttons = document.querySelectorAll("button");
buttons.forEach((btn, index) => {
btn.addEventListener("click", erstelleButtonHandler(`Button ${index + 1}`));
});
4. Module Pattern
const UserModule = (function() {
// Private Variablen
let users = [];
let nextId = 1;
// Private Funktion
function generateId() {
return nextId++;
}
// Öffentliche API
return {
add: function(name) {
const user = {
id: generateId(),
name: name
};
users.push(user);
return user;
},
remove: function(id) {
users = users.filter(u => u.id !== id);
},
getAll: function() {
return [...users]; // Kopie zurückgeben
},
find: function(id) {
return users.find(u => u.id === id);
}
};
})();
UserModule.add("Max");
UserModule.add("Lisa");
console.log(UserModule.getAll());
// [{ id: 1, name: "Max" }, { id: 2, name: "Lisa" }]
// Private Variablen nicht zugänglich
console.log(UserModule.users); // undefined
console.log(UserModule.nextId); // undefined
5. Memoization (Caching)
function memoize(fn) {
const cache = {}; // Closure über cache
return function(...args) {
const key = JSON.stringify(args);
if (cache[key] !== undefined) {
console.log("Aus Cache:", key);
return cache[key];
}
console.log("Berechne:", key);
const result = fn(...args);
cache[key] = result;
return result;
};
}
// Teure Berechnung
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const memoFib = memoize(function fib(n) {
if (n <= 1) return n;
return memoFib(n - 1) + memoFib(n - 2);
});
console.log(memoFib(40)); // Schnell dank Cache!
6. Debounce
function debounce(fn, delay) {
let timeoutId; // Closure über timeoutId
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// Verwendung: Nur ausführen wenn 300ms keine neue Eingabe
const searchInput = document.querySelector("#search");
const debouncedSearch = debounce((value) => {
console.log("Suche nach:", value);
}, 300);
searchInput.addEventListener("input", (e) => {
debouncedSearch(e.target.value);
});
7. Throttle
function throttle(fn, limit) {
let inThrottle = false;
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(() => {
inThrottle = false;
}, limit);
}
};
}
// Verwendung: Maximal alle 100ms ausführen
window.addEventListener("scroll", throttle(() => {
console.log("Scroll Position:", window.scrollY);
}, 100));
8. Once - Nur einmal ausführen
function once(fn) {
let called = false;
let result;
return function(...args) {
if (!called) {
called = true;
result = fn.apply(this, args);
}
return result;
};
}
const initializeOnce = once(() => {
console.log("Initialisierung...");
return { initialized: true };
});
initializeOnce(); // "Initialisierung..."
initializeOnce(); // (keine Ausgabe)
initializeOnce(); // (keine Ausgabe)
Häufiges Problem: Closure in Schleifen
Das Problem
// ❌ Klassischer Fehler mit var
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // Alle loggen 3!
}, 1000);
}
// Ausgabe: 3, 3, 3
// Warum? var ist function-scoped, nicht block-scoped.
// Alle Callbacks teilen sich dasselbe i.
// Wenn sie ausgeführt werden, ist i bereits 3.
Lösung 1: let verwenden
// ✅ Mit let - jede Iteration hat eigenes i
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Ausgabe: 0, 1, 2
Lösung 2: IIFE (vor ES6)
// ✅ Sofort ausgeführte Funktion erstellt neuen Scope
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j);
}, 1000);
})(i);
}
// Ausgabe: 0, 1, 2
Lösung 3: Closure erstellen
// ✅ Funktion erstellt Closure für jedes i
function createCallback(i) {
return function() {
console.log(i);
};
}
for (var i = 0; i < 3; i++) {
setTimeout(createCallback(i), 1000);
}
// Ausgabe: 0, 1, 2
Closure Memory Leaks
Closures können Memory Leaks verursachen, wenn sie Referenzen halten, die nicht mehr gebraucht werden:
// ⚠️ Potentieller Memory Leak
function setupHandler() {
const grossesDatenArray = new Array(1000000).fill("data");
document.querySelector("#button").addEventListener("click", function() {
// Diese Funktion hält Referenz zu grossesDatenArray!
console.log("Klick!");
});
}
// ✅ Besser: Nur benötigte Daten in Closure
function setupHandler() {
const grossesDatenArray = new Array(1000000).fill("data");
const benoetigteDaten = grossesDatenArray[0]; // Nur was wir brauchen
document.querySelector("#button").addEventListener("click", function() {
console.log(benoetigteDaten);
});
// grossesDatenArray kann jetzt garbage collected werden
}
Closure vs. Klassen
Beide können ähnliche Dinge erreichen:
// Mit Closure
function createCounter() {
let count = 0;
return {
increment() { return ++count; },
decrement() { return --count; },
getCount() { return count; }
};
}
// Mit Klasse
class Counter {
#count = 0; // Private field (modern)
increment() { return ++this.#count; }
decrement() { return --this.#count; }
getCount() { return this.#count; }
}
// Verwendung ist fast identisch
const closure = createCounter();
const instance = new Counter();
closure.increment();
instance.increment();
Zusammenfassung
| Konzept | Beschreibung |
|---|---|
| Scope | Sichtbarkeitsbereich von Variablen |
| Closure | Funktion + ihr Scope (Umgebung) |
| Lexikalisch | Scope wird zur Definition bestimmt |
| Private Daten | Closure schützt Variablen |
| Memory | Vorsicht bei großen gecloseten Daten |
// Das Closure-Pattern in einem Satz:
// Eine Funktion "erinnert" sich an Variablen aus ihrem Geburts-Scope.
function geburtsort() {
const erinnerung = "Ich war hier!";
return function() {
return erinnerung; // Closure!
};
}
const fn = geburtsort();
console.log(fn()); // "Ich war hier!"
Übung: Erstelle eine createStack Funktion, die einen Stack mit push, pop, peek und size Methoden zurückgibt. Die interne Array-Struktur soll privat sein!