Der Moment, in dem es Klick gemacht hat
Vor zwei Jahren hat mir ein Senior-Developer gesagt: “Wenn du Closures verstehst, verstehst du JavaScript.”
Ich habe nur Bahnhof verstanden. Closures? Klingt kompliziert.
Heute sage ich: Er hatte Recht. Scopes und Closures zu verstehen hat meinen Code fundamental verbessert. Lass mich dir zeigen, warum.
Was ist Scope überhaupt?
Scope bestimmt, wo Variablen sichtbar sind.
// Global Scope - überall sichtbar
const globalName = 'Max';
function greet() {
// Function Scope - nur in dieser Funktion
const localName = 'Anna';
console.log(globalName); // OK - greift auf global zu
console.log(localName); // OK - lokal verfügbar
}
greet();
console.log(globalName); // OK
console.log(localName); // ReferenceError: localName is not defined
Die drei Scope-Typen
// 1. Global Scope
const global = 'Überall sichtbar';
function demo() {
// 2. Function Scope
const functionScoped = 'Nur in dieser Funktion';
if (true) {
// 3. Block Scope (let/const)
const blockScoped = 'Nur in diesem Block';
var functionScoped2 = 'Function-scoped trotz Block!';
console.log(blockScoped); // OK
}
console.log(blockScoped); // ReferenceError
console.log(functionScoped2); // OK (var ignoriert Block!)
}
Key Takeaway: Nutze const/let (block-scoped), niemals var (function-scoped).
Mehr Details: Check mein Tutorial zu Variablen und Scopes.
Scope Chain: Wie JavaScript Variablen findet
const level1 = 'Global';
function outer() {
const level2 = 'Outer';
function inner() {
const level3 = 'Inner';
// JavaScript sucht in dieser Reihenfolge:
console.log(level3); // 1. Findet es in inner() ✓
console.log(level2); // 2. Findet es in outer() ✓
console.log(level1); // 3. Findet es in global ✓
console.log(level4); // 4. Nicht gefunden → ReferenceError
}
inner();
}
outer();
Shadowing: Wenn Namen kollidieren
const name = 'Global Max';
function greet() {
const name = 'Local Max'; // "Überschattet" die globale Variable
console.log(name); // "Local Max"
}
greet();
console.log(name); // "Global Max"
Closures: Die Superkraft
Definition: Eine Closure ist eine Funktion, die Zugriff auf Variablen aus ihrem äußeren Scope hat, auch nachdem die äußere Funktion beendet wurde.
Klingt kompliziert? Hier ist ein Beispiel:
function createCounter() {
let count = 0; // Private Variable
return function() {
count++; // Greift auf 'count' aus äußerem Scope zu
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// count ist NICHT direkt zugänglich
console.log(count); // ReferenceError
Was passiert hier?
createCounter()wird aufgerufen und erstelltcount = 0- Eine Funktion wird zurückgegeben (die innere Funktion)
createCounter()ist beendet, abercountlebt weiter- Die innere Funktion “erinnert” sich an
count(Closure!)
Praktische Closure-Patterns
1. Private Variables (Data Encapsulation)
// ❌ Ohne Closure: Alles public
const user = {
password: '12345', // Jeder kann es lesen/ändern!
checkPassword: function(input) {
return input === this.password;
}
};
console.log(user.password); // "12345" (nicht gut!)
user.password = 'hacked'; // Kann überschrieben werden!
// ✅ Mit Closure: Private Daten
function createUser(initialPassword) {
// password ist privat - keine direkte Zugriff möglich
let password = initialPassword;
return {
checkPassword: function(input) {
return input === password;
},
changePassword: function(oldPass, newPass) {
if (oldPass === password) {
password = newPass;
return true;
}
return false;
}
};
}
const user2 = createUser('12345');
console.log(user2.password); // undefined (nicht zugänglich!)
user2.password = 'hacked'; // Hat keine Wirkung
console.log(user2.checkPassword('12345')); // true
2. Factory Functions
// Erstelle konfigurierbare Funktionen
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
const times10 = createMultiplier(10);
console.log(double(5)); // 10
console.log(triple(5)); // 15
console.log(times10(5)); // 50
// Jede Funktion "erinnert" sich an ihren eigenen 'factor'!
3. Event Handlers mit Closure
// Problem: Loop mit var
for (var i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 4, 4, 4 (nicht 1, 2, 3!)
}, i * 1000);
}
// Warum? Alle Callbacks teilen sich die GLEICHE Variable 'i'
// Wenn setTimeout ausgeführt wird, ist i bereits 4
// ✅ Lösung 1: let statt var (Block Scope)
for (let i = 1; i <= 3; i++) {
setTimeout(function() {
console.log(i); // 1, 2, 3 (Jede Iteration hat eigenes 'i')
}, i * 1000);
}
// ✅ Lösung 2: IIFE (Immediately Invoked Function Expression)
for (var i = 1; i <= 3; i++) {
(function(capturedI) {
setTimeout(function() {
console.log(capturedI); // 1, 2, 3
}, capturedI * 1000);
})(i);
}
4. Module Pattern
// Klassisches Module Pattern mit Closure
const ShoppingCart = (function() {
// Private Variablen
let items = [];
let total = 0;
// Private Funktion
function calculateTotal() {
total = items.reduce((sum, item) => sum + item.price, 0);
}
// Public API
return {
addItem: function(item) {
items.push(item);
calculateTotal();
},
removeItem: function(itemName) {
items = items.filter(item => item.name !== itemName);
calculateTotal();
},
getTotal: function() {
return total;
},
getItems: function() {
// Return copy, nicht Original (Encapsulation)
return [...items];
}
};
})();
// Usage
ShoppingCart.addItem({ name: 'Book', price: 20 });
ShoppingCart.addItem({ name: 'Pen', price: 5 });
console.log(ShoppingCart.getTotal()); // 25
// items und total sind NICHT zugänglich
console.log(ShoppingCart.items); // undefined
console.log(ShoppingCart.total); // undefined
5. Memoization (Caching)
// Teure Berechnung cachen mit Closure
function createMemoizedFunction(fn) {
const cache = {}; // Private Cache
return function(arg) {
if (cache[arg] !== undefined) {
console.log('From cache:', arg);
return cache[arg];
}
console.log('Computing:', arg);
const result = fn(arg);
cache[arg] = result;
return result;
};
}
// Beispiel: Fibonacci (langsam ohne Cache)
const fibonacci = createMemoizedFunction(function(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
});
console.log(fibonacci(10)); // Computing: 10, 9, 8, ... (erste Berechnung)
console.log(fibonacci(10)); // From cache: 10 (sofort!)
6. Curry Functions
// Currying: Funktion, die Funktion zurückgibt
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
}
return function(...nextArgs) {
return curried.apply(this, args.concat(nextArgs));
};
};
}
// Normale Funktion
function add(a, b, c) {
return a + b + c;
}
// Curried Version
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 6
console.log(curriedAdd(1, 2)(3)); // 6
console.log(curriedAdd(1)(2, 3)); // 6
// Praktisch für Partial Application
const add10 = curriedAdd(10);
console.log(add10(5, 3)); // 18
console.log(add10(2, 8)); // 20
React Hooks: Closures in Action
Wenn du React nutzt, nutzt du Closures ständig:
function Counter() {
const [count, setCount] = useState(0);
// Diese Funktion hat Zugriff auf 'count' via Closure
const increment = () => {
setCount(count + 1);
};
// setTimeout hat auch Closure über 'count'
useEffect(() => {
const timer = setTimeout(() => {
console.log('Count after 1 second:', count);
}, 1000);
return () => clearTimeout(timer);
}, [count]);
return <button onClick={increment}>{count}</button>;
}
Stale Closure Problem in React
function Counter() {
const [count, setCount] = useState(0);
// ❌ Problem: Closure über altes 'count'
const incrementBroken = () => {
setTimeout(() => {
setCount(count + 1); // Nutzt 'count' aus dem Render wo Button geklickt wurde!
}, 1000);
};
// ✅ Lösung: Functional Update
const incrementFixed = () => {
setTimeout(() => {
setCount(prevCount => prevCount + 1); // Immer aktueller Wert
}, 1000);
};
return <button onClick={incrementFixed}>{count}</button>;
}
Memory Leaks durch Closures vermeiden
Problem: Event Listeners
// ❌ Memory Leak
function attachEventWithLeak() {
const largeData = new Array(1000000).fill('data');
document.getElementById('button').addEventListener('click', function() {
console.log('Button clicked');
// Closure hält Referenz auf largeData, auch wenn nicht genutzt!
});
}
// ✅ Fix: Cleanup
function attachEventProperly() {
const largeData = new Array(1000000).fill('data');
function handleClick() {
console.log('Button clicked');
// largeData nicht referenziert = kann garbage collected werden
}
const button = document.getElementById('button');
button.addEventListener('click', handleClick);
// Cleanup
return function cleanup() {
button.removeEventListener('click', handleClick);
};
}
Regel: Nur referenzieren was du brauchst
function createProcessor(data) {
// ❌ Closure hält ALLE data
return function() {
return data.filter(item => item.active);
};
// ✅ Closure hält nur was nötig ist
const activeIds = data.filter(item => item.active).map(item => item.id);
return function() {
return activeIds;
};
}
Debugging von Closures
Chrome DevTools
function outer() {
const secret = 'Hidden Value';
function inner() {
debugger; // Breakpoint setzen
console.log(secret);
}
return inner;
}
const fn = outer();
fn();
// In Chrome DevTools:
// 1. Execution pausiert bei 'debugger'
// 2. Scope Panel zeigt:
// - Local (inner function scope)
// - Closure (outer function scope mit 'secret')
// - Global
Console Tricks
function createCounter() {
let count = 0;
return {
increment: function() {
count++;
return count;
},
debug: function() {
console.log('Current count:', count);
console.log('Closure variables:', { count });
}
};
}
const counter = createCounter();
counter.increment();
counter.increment();
counter.debug(); // Zeigt interne State
Best Practices
1. Benenne Funktionen für besseres Debugging
// ❌ Anonyme Funktion
const counter = (function() {
let count = 0;
return function() { return ++count; };
})();
// ✅ Benannte Funktionen
const counter2 = (function createCounter() {
let count = 0;
return function increment() {
return ++count;
};
})();
// In Stack Trace siehst du "increment" statt "anonymous"
2. Vermeide Closures in Loops (wenn nicht nötig)
// ❌ Erstellt unnötige Closures
const buttons = Array.from(document.querySelectorAll('button'));
buttons.forEach((button, index) => {
button.addEventListener('click', function() {
console.log('Button', index, 'clicked');
});
});
// ✅ Besser: data-attribute
buttons.forEach((button, index) => {
button.dataset.index = index;
button.addEventListener('click', handleButtonClick);
});
function handleButtonClick() {
console.log('Button', this.dataset.index, 'clicked');
}
3. Dokumentiere komplexe Closures
/**
* Erstellt einen Rate Limiter mit Closure-based State
* @param {Function} fn - Die zu limitierende Funktion
* @param {number} limit - Max Aufrufe pro Zeitfenster
* @param {number} window - Zeitfenster in ms
*/
function createRateLimiter(fn, limit, window) {
let calls = []; // Closure: Speichert Timestamps
return function(...args) {
const now = Date.now();
calls = calls.filter(time => now - time < window);
if (calls.length < limit) {
calls.push(now);
return fn.apply(this, args);
}
throw new Error('Rate limit exceeded');
};
}
Fazit
Scopes und Closures zu verstehen ist fundamental für JavaScript:
Scopes
- Block Scope mit
let/const(bevorzugt) - Function Scope mit
var(vermeiden) - Global Scope (sparsam nutzen)
- Scope Chain bestimmt Variable Lookup
Closures
- Funktion + Outer Scope = Closure
- Private Variables durch Closures
- Factory Functions für Konfiguration
- Module Pattern für Encapsulation
- React Hooks nutzen Closures intensiv
Vorteile
- Data Encapsulation
- Private State
- Memoization/Caching
- Flexible APIs
- Functional Programming
Mein Code wurde durch Closures:
- 50% lesbarer (klare Abstraktion)
- 30% weniger Bugs (private State)
- Wartbarer (gekapselte Logik)
Weiterführende Ressourcen
- Variablen und Scopes Tutorial
- JavaScript Grundlagen
- You Don’t Know JS - Scope & Closures
- MDN - Closures
- JavaScript.info - Closures
Wie nutzt du Closures in deinem Code? Teile deine Use Cases!