13 Closures und Anwendungsmuster

Closures gehören zu den leistungsstärksten und oft missverstandenen Konzepten in JavaScript. Eine Closure entsteht, wenn eine Funktion auf Variablen aus ihrem äußeren lexikalischen Scope zugreifen kann, selbst wenn diese Funktion außerhalb ihres ursprünglichen Kontexts ausgeführt wird. Dieses Konzept ermöglicht elegante Lösungen für viele Programmierprobleme und bildet die Grundlage für zahlreiche fortgeschrittene JavaScript-Muster.

13.1 Das Grundprinzip von Closures

Eine Closure entsteht, wenn eine innere Funktion auf Variablen ihrer äußeren (umgebenden) Funktion zugreift, auch nachdem die äußere Funktion bereits abgeschlossen wurde:

function äußereFunktion() {
  const nachricht = "Hallo von der äußeren Funktion";
  
  function innereFunktion() {
    console.log(nachricht); // Greift auf Variable der äußeren Funktion zu
  }
  
  return innereFunktion; // Gibt die innere Funktion zurück
}

const meineClosure = äußereFunktion();
meineClosure(); // "Hallo von der äußeren Funktion"

Im obigen Beispiel: 1. äußereFunktion definiert eine lokale Variable nachricht und eine innere Funktion 2. Die innere Funktion verwendet nachricht aus dem Scope der äußeren Funktion 3. äußereFunktion gibt die innere Funktion zurück 4. Die Ausführung von äußereFunktion ist abgeschlossen, aber die zurückgegebene innere Funktion kann immer noch auf nachricht zugreifen

Diese Fähigkeit der inneren Funktion, den lexikalischen Umgebungszustand “einzufangen” und zu bewahren, macht sie zu einer Closure.

13.2 Wie Closures funktionieren

Um Closures zu verstehen, müssen wir betrachten, wie JavaScript mit Funktionen und deren lexikalischen Umgebungen umgeht:

  1. Lexikalische Umgebung: Jede Funktion hat eine Referenz auf ihre lexikalische Umgebung (den Scope, in dem sie definiert wurde).

  2. Variablen-Lebensdauer: Normalerweise werden die lokalen Variablen einer Funktion freigegeben, wenn die Funktion beendet wird. Bei Closures bleibt die lexikalische Umgebung jedoch erhalten, solange eine Referenz auf die darin definierte innere Funktion existiert.

  3. Gemeinsame Umgebung: Wenn mehrere innere Funktionen im selben äußeren Scope definiert werden, teilen sie sich dieselbe lexikalische Umgebung:

function zählerErstellen() {
  let zählerstand = 0;
  
  return {
    erhöhen: function() {
      return ++zählerstand;
    },
    verringern: function() {
      return --zählerstand;
    },
    wert: function() {
      return zählerstand;
    }
  };
}

const zähler = zählerErstellen();
console.log(zähler.erhöhen()); // 1
console.log(zähler.erhöhen()); // 2
console.log(zähler.verringern()); // 1
console.log(zähler.wert()); // 1

Beide Funktionen, erhöhen und verringern, greifen auf dieselbe zählerstand-Variable zu und teilen sich deren Zustand.

13.3 Anwendungsmuster für Closures

Closures ermöglichen zahlreiche nützliche Programmiermuster, die die Codebasis strukturierter, modularer und wartbarer machen.

13.3.1 Datenprivatsheit und Kapselung

Eine der häufigsten Anwendungen von Closures ist die Implementierung privater Variablen und Methoden:

function bankkontoErstellen(anfangsguthaben = 0) {
  let guthaben = anfangsguthaben; // Private Variable
  
  // Öffentliche API
  return {
    einzahlen(betrag) {
      if (betrag > 0) {
        guthaben += betrag;
        return true;
      }
      return false;
    },
    abheben(betrag) {
      if (betrag > 0 && betrag <= guthaben) {
        guthaben -= betrag;
        return true;
      }
      return false;
    },
    kontostand() {
      return guthaben;
    }
  };
}

const konto = bankkontoErstellen(100);
console.log(konto.kontostand()); // 100
konto.einzahlen(50);
console.log(konto.kontostand()); // 150
konto.abheben(30);
console.log(konto.kontostand()); // 120
// konto.guthaben = 1000000; // Kein direkter Zugriff möglich!

Dieses Muster, oft als “Modul-Muster” bezeichnet, war vor ES6-Modulen eine Hauptmethode zur Implementierung von Kapselung in JavaScript.

13.3.2 Funktionsfabriken

Closures ermöglichen die Erstellung von Funktionsfabriken – Funktionen, die spezialisierte Funktionen zurückgeben:

function multiplier(faktor) {
  return function(zahl) {
    return zahl * faktor;
  };
}

const verdoppeln = multiplier(2);
const verdreifachen = multiplier(3);
const verzehnfachen = multiplier(10);

console.log(verdoppeln(5));     // 10
console.log(verdreifachen(5));  // 15
console.log(verzehnfachen(5));  // 50

Diese Technik ermöglicht es, Funktionen mit teilweise angewendeten Parametern zu erstellen, was zu präziseren und ausdrucksstärkeren APIs führt.

13.3.3 Callback-Konfiguration

Closures sind besonders nützlich, um Callbacks mit zusätzlichen Informationen oder Konfigurationen auszustatten:

function eventHandler(element, event) {
  return function(callback) {
    element.addEventListener(event, callback);
  };
}

const buttonClick = eventHandler(document.getElementById('button'), 'click');
buttonClick(function() {
  console.log('Button wurde geklickt!');
});

13.3.4 Memoization (Caching von Funktionsergebnissen)

Closures ermöglichen die Implementierung von Memoization – dem Caching von Funktionsergebnissen für wiederholte Aufrufe:

function memoize(fn) {
  const cache = {};
  
  return function(...args) {
    const key = JSON.stringify(args);
    
    if (key in cache) {
      console.log('Ergebnis aus Cache');
      return cache[key];
    } else {
      console.log('Berechne Ergebnis');
      const result = fn.apply(this, args);
      cache[key] = result;
      return result;
    }
  };
}

// Beispiel: Fibonacci-Berechnung mit Memoization
const fibonacci = memoize(function(n) {
  if (n <= 1) return n;
  return fibonacci(n - 1) + fibonacci(n - 2);
});

console.log(fibonacci(10)); // Effizient berechnet mit Caching

Das Ergebnis für jeden spezifischen Eingabewert wird nur einmal berechnet und dann bei zukünftigen Aufrufen aus dem Cache abgerufen, was die Leistung erheblich verbessern kann.

13.3.5 Iteratoren und Generatoren erstellen

Vor der Einführung von ES6-Generatoren wurden Iteratoren oft mit Closures implementiert:

function rangeIterator(start, end, step = 1) {
  let current = start;
  
  return {
    hasNext: function() {
      return current <= end;
    },
    next: function() {
      const value = current;
      current += step;
      return value;
    }
  };
}

const iterator = rangeIterator(1, 5);
while (iterator.hasNext()) {
  console.log(iterator.next()); // 1, 2, 3, 4, 5
}

13.3.6 Zustandserhaltung zwischen Aufrufen

Closures können verwendet werden, um Zustände zwischen Funktionsaufrufen zu erhalten, ohne globale Variablen zu verwenden:

function createLogger(logPrefix) {
  let logCount = 0;
  
  return function(message) {
    logCount++;
    console.log(`${logPrefix} (${logCount}): ${message}`);
  };
}

const debugLog = createLogger('DEBUG');
const errorLog = createLogger('ERROR');

debugLog('App gestartet'); // "DEBUG (1): App gestartet"
debugLog('Daten geladen'); // "DEBUG (2): Daten geladen"
errorLog('Verbindungsfehler'); // "ERROR (1): Verbindungsfehler"

Jede Logger-Instanz behält ihren eigenen logCount-Zustand bei, unabhängig von anderen Instanzen.

13.3.7 Currying und partielle Anwendung

Currying ist eine Technik, bei der eine Funktion mit mehreren Parametern in eine Sequenz von Funktionen mit je einem Parameter umgewandelt wird:

// Normale Funktion mit drei Parametern
function add(a, b, c) {
  return a + b + c;
}

// Gecurryte Version
function curriedAdd(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
}

console.log(curriedAdd(1)(2)(3)); // 6

// Teilweise Anwendung
const add1 = curriedAdd(1);
const add1And2 = add1(2);
console.log(add1And2(3)); // 6

Diese Technik ist besonders in der funktionalen Programmierung nützlich und ermöglicht die schrittweise Spezialisierung von Funktionen.

13.3.8 Module-Muster (vor ES6)

Vor ES6-Modulen wurde das IIFE (Immediately Invoked Function Expression) Module-Muster mit Closures verwendet, um modularen Code zu erstellen:

const MeinModul = (function() {
  // Private Variablen und Funktionen
  let zähler = 0;
  
  function private1() {
    return 'Ich bin privat';
  }
  
  // Öffentliche API
  return {
    erhöhen: function() {
      return ++zähler;
    },
    wert: function() {
      return zähler;
    },
    grüßen: function(name) {
      return `Hallo ${name}, ${private1()}`;
    }
  };
})();

console.log(MeinModul.wert()); // 0
console.log(MeinModul.erhöhen()); // 1
console.log(MeinModul.grüßen('Max')); // "Hallo Max, Ich bin privat"
// console.log(MeinModul.zähler); // undefined - Variable ist privat
// console.log(MeinModul.private1()); // Error - Funktion ist privat

Mit ES6-Modulen ist dieses Muster weniger notwendig geworden, bleibt aber ein historisch wichtiges Beispiel für Closures.

13.4 Häufige Fallstricke bei Closures

Trotz ihrer Leistungsfähigkeit können Closures bei unsachgemäßer Verwendung zu einigen typischen Problemen führen.

13.4.1 Schleifenvariablen in Closures (vor ES6)

Ein klassisches Problem mit Closures tritt in Schleifen auf:

// Problematischer Code
function createFunctions() {
  var funcs = [];
  
  for (var i = 0; i < 3; i++) {
    funcs.push(function() {
      console.log(i);
    });
  }
  
  return funcs;
}

var functions = createFunctions();
functions[0](); // 3 (nicht 0 wie vielleicht erwartet)
functions[1](); // 3
functions[2](); // 3

Das Problem ist, dass alle Funktionen in funcs auf dieselbe i-Variable verweisen, die nach der Schleife den Wert 3 hat.

Lösungsansätze:

  1. Mit IIFE (vor ES6):
for (var i = 0; i < 3; i++) {
  funcs.push((function(index) {
    return function() {
      console.log(index);
    };
  })(i));
}
  1. Mit ES6 let:
for (let i = 0; i < 3; i++) { // let erstellt für jede Iteration einen neuen Scope
  funcs.push(function() {
    console.log(i);
  });
}

Die let-Lösung ist eleganter und wird in modernem JavaScript bevorzugt.

13.4.2 Speicherlecks

Da Closures Referenzen auf ihre äußere lexikalische Umgebung behalten, können sie unbeabsichtigt Speicherlecks verursachen:

function potentiellesSpeicherleck() {
  const großesDatenArray = new Array(1000000).fill('Daten');
  
  return function() {
    console.log(großesDatenArray.length);
  };
}

Selbst wenn nur ein kleiner Teil des großen Arrays benötigt wird, bleibt das gesamte Array im Speicher, solange die zurückgegebene Funktion existiert.

Besser:

function keinSpeicherleck() {
  const großesDatenArray = new Array(1000000).fill('Daten');
  const länge = großesDatenArray.length; // Nur den benötigten Wert speichern
  
  return function() {
    console.log(länge);
  };
}

13.4.3 Performance-Überlegungen

Closures haben einen leichten Overhead bezüglich Speicher und Leistung. Für die meisten Anwendungen ist dies vernachlässigbar, aber in leistungskritischen Schleifen oder Funktionen mit häufigem Aufruf sollten Alternativen erwogen werden.

13.5 Fortgeschrittene Anwendungsmuster

13.5.1 Funktionales Programmieren mit Closures

Closures bilden die Grundlage für viele funktionale Programmiermuster in JavaScript:

// Komposition von Funktionen
function compose(...functions) {
  return function(x) {
    return functions.reduceRight((value, func) => func(value), x);
  };
}

const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;

const pipeline = compose(square, double, addOne);
console.log(pipeline(3)); // ((3 + 1) * 2)² = 64

13.5.2 Lazy Evaluation (verzögerte Auswertung)

Closures können verwendet werden, um Berechnungen zu verzögern, bis ihre Ergebnisse tatsächlich benötigt werden:

function lazyValue(computeFn) {
  let cachedValue;
  let computed = false;
  
  return function() {
    if (!computed) {
      cachedValue = computeFn();
      computed = true;
    }
    return cachedValue;
  };
}

const expensiveComputation = lazyValue(function() {
  console.log('Führe aufwändige Berechnung durch...');
  return Math.pow(2, 10);
});

console.log('Ergebnis wird noch nicht berechnet');
// Irgendwann später, wenn das Ergebnis benötigt wird:
console.log(expensiveComputation()); // "Führe aufwändige Berechnung durch..." dann 1024
console.log(expensiveComputation()); // Gibt direkt 1024 zurück (keine erneute Berechnung)

13.5.3 Dekoratoren-Muster

Closures ermöglichen die Implementierung des Dekorator-Designmusters, bei dem Funktionen mit zusätzlichem Verhalten “dekoriert” werden:

function withLogging(fn) {
  return function(...args) {
    console.log(`Funktion aufgerufen mit Argumenten: ${args}`);
    const result = fn.apply(this, args);
    console.log(`Funktion gab zurück: ${result}`);
    return result;
  };
}

function add(a, b) {
  return a + b;
}

const loggingAdd = withLogging(add);
loggingAdd(2, 3); 
// "Funktion aufgerufen mit Argumenten: 2,3"
// "Funktion gab zurück: 5"

13.5.4 Datenanalyse mit Closures

Closures können für datenverarbeitende Pipelines verwendet werden:

function filter(predicate) {
  return function(array) {
    return array.filter(predicate);
  };
}

function map(transform) {
  return function(array) {
    return array.map(transform);
  };
}

function reduce(reducer, initialValue) {
  return function(array) {
    return array.reduce(reducer, initialValue);
  };
}

const zahlen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

const pipeline = [
  filter(n => n % 2 === 0),        // Filtere gerade Zahlen
  map(n => n * n),                 // Quadrieren
  reduce((sum, n) => sum + n, 0)   // Summe berechnen
];

const ergebnis = pipeline.reduce((data, fn) => fn(data), zahlen);
console.log(ergebnis); // 220 (4² + 6² + 8² + 10² = 16 + 36 + 64 + 100)

13.6 Closures in modernem JavaScript

Mit der Einführung von ES6-Modulen, Klassen und lexikalischem this in Pfeilfunktionen hat sich die Rolle von Closures in JavaScript etwas verändert:

ES6-Module bieten jetzt eine native Möglichkeit, privaten Code zu kapseln:

// counter.js
let count = 0; // Privat für dieses Modul

export function increment() {
  return ++count;
}

export function getValue() {
  return count;
}

Private Klassenfelder (ab ES2022) reduzieren den Bedarf an Closures für Datenprivatsheit:

class Counter {
  #count = 0; // Privates Feld
  
  increment() {
    return ++this.#count;
  }
  
  get value() {
    return this.#count;
  }
}

Dennoch bleiben Closures ein fundamentales Konzept in JavaScript und werden weiterhin extensiv in funktionalen Programmierparadigmen, für Memoization, und in vielen fortgeschrittenen Mustern verwendet.