JavaScript Fortgeschritten

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?

  1. erstelleZaehler() wird aufgerufen
  2. count wird mit 0 initialisiert
  3. Eine innere Funktion wird zurückgegeben
  4. erstelleZaehler() ist fertig - aber count lebt weiter!
  5. Die innere Funktion “schließt über” (closes over) count
  6. Jeder Aufruf von zaehler() hat Zugriff auf dasselbe count

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

KonzeptBeschreibung
ScopeSichtbarkeitsbereich von Variablen
ClosureFunktion + ihr Scope (Umgebung)
LexikalischScope wird zur Definition bestimmt
Private DatenClosure schützt Variablen
MemoryVorsicht 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!