Closures (Funktionsabschlüsse)

Closures werden bei JavaScript gerne als ein fortgeschrittenes Feature angesehen. Sie zu verstehen ist jedoch wichtig, um die Sprache zu meistern.

Im einem Satz: Variablen, die innerhalb eines Sichtbarkeitsbereichs einer äußeren Funktion liegen, können von einer inneren Funktionsdefinition aus angesprochen werden.

function init() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  displayName();
}
init();

Die init()-Funktion erstellt eine lokale Variable name und definiert dann eine Funktion displayName(). Diese ist eine innere Funktion - sie ist im Sichtbarkeitsbereich von init() definiert und nur innerhalb dieses Bereichs verfügbar. Die Funktion displayName() besitzt selbst keine lokalen Variablen, greift jedoch auf die name-Variable der äußeren Funktion zu.

Dies funktioniert problemlos - probieren Sie den Code doch einfach selbst aus, um zu sehen was passiert! Es ist ein Beispiel für functional scoping: Bei JavaScript wird der Sichtbarkeitsbereich einer Variablen durch die Position im Quellcode bestimmt und eingebettete Funktionen haben Zugriff auf Variablen des äußeren Sichtbarkeitsbereichs.

Nun ein weiteres Beispiel:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
}

var myFunc = makeFunc();
myFunc();

Wenn dieser Code ausgeführt wird, passiert so ziemlich das Gleiche wie beim vorherigen init()-Beispiel: Der String "Mozilla" wird als Alarmmeldung ausgegeben. Der Unterschied der beiden Beispiele besteht darin, dass die innere displayName()-Funktion über return zurückgegeben wird, bevor diese ausgeführt wird.

Es mag etwas merkwürdig erscheinen, dass dieser Code tatsächlich funktioniert. Normalerweise existiert eine lokale Variable innerhalb einer Funktion nur solange die Funktion ausgeführt wird. Man könnte also davon ausgehen, dass nachdem makeFunc() ausgeführt wurde, die name-Variable nicht länger bestehen bleibt. Da der Code aber wie erwartet funktioniert, ist dies offensichtlich nicht der Fall.

Des Rätsels Lösung: Die Funktion myFunc wurde zu einer Closure - einer speziellen Art von Objekt, das zwei Dinge kombiniert: eine Funktion und die Umgebung, in der die Funktion erstellt wurde. Die Umgebung schließt alle lokalen Variablen ein, die zur Zeit der Closure-Erstellung im selben Sichtbarkeitsbereich lagen. Bei unserem Beispiel ist myFunc also eine Closure, die beides einbezieht - die displayName-Funktion und den String "Mozilla", welcher bei Erstellung der Closure in der Umgebung existierte.

Hier ein noch etwas interessanteres Beispiel - eine makeAdder-Funktion:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);

print(add5(2));  // 7
print(add10(2)); // 12

Bei diesem Beispiel wurde eine Funktion makeAdder(x) definiert, die ein einziges Argument x entgegennimmt und eine neue Funktion zurückgibt. Diese zurückgegebene Funktion nimmt wiederum ein Argument y entgegen und gibt die Summe von x und y zurück.

Alles in allem ist die makeAdder-Funktion eine factory - sie „produziert“ Funktionen, die einen bestimmten Wert mit dem Argument addieren. Bei dem Beispiel oben benutzen wir unsere "Funktionsfabrik", um zwei neue Funktionen zu erstellen - eine, die 5 zum Argument addiert und eine weitere, die 10 addiert.

add5 und add10 sind beide Closures - sie haben die gleiche Funktionsdefinition, aber beziehen ihre Werte aus unterschiedlichen Umgebungen. In der Umgebung von add5 entspricht x dem Wert 5; in der Umgebung von add10 hingegen dem Wert 10.

Praktische Closures

Theorie beiseite und ein mal nachgefragt: Sind denn Closures tatsächlich auch nützlich? Finden wir es heraus, indem wir Überlegungen zum praktischen Nutzen von Closures anstellen. Eine Closure erlaubt die Verknüpfung von Daten (aus der Umgebung) mit einer Funktion, die mit diesen Daten arbeitet. Dies hat Ähnlichkeit mit objektorientierter Programmierung, wo Daten über Objekte (die Eigenschaften des Objekts) mit Methoden verknüpft werden.

Konsequenterweise kann man eine Closure überall dort einsetzen, wo man normalerweise ein Objekt, welches nur eine einzige Methode besitzt, verwenden würde.

Solche Situationen kommen bei der Web-Programmierung immer wieder vor, denn ein Großteil des Codes ist Event-basiert - es wird ein bestimmter Vorgang definiert und dann an ein Event gebunden, das vom Benutzer ausgelöst wird (wie z. B. ein Klick oder Tastendruck). Der Code wird in der Regel als Callback angebunden: eine einzelne Funktion, die als Reaktion auf das Event ausgeführt wird.

Im Folgenden ein praktisches Beispiel: Angenommen man möchte einer Seite ein paar Buttons hinzufügen, womit die Schriftgröße eingestellt werden kann. Eine Möglichkeit dazu wäre, für die Schriftgröße des body-Elements einen Pixelwert festzulegen und für andere Elemente auf der Seite (wie z. B. Header) einen relativen em-Wert zu spezifizieren:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}
h2 {
  font-size: 1.2em;
}

Mit den interaktiven Buttons kann der Benutzer dann die Schriftgröße für das body-Element einstellen und die Schriftgröße der anderen Elemente passt sich augrund der relativen Werte entsprechend an.

Nun der JavaScript-Code:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

size12, size14 und size16 sind Funktionen, welche die Schriftgröße des body auf 12, 14 oder 16 Pixel setzen. Die Funktionen können mit Buttons (in diesem Fall Links) wie folgt verknüpft werden:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a>

Sicht auf jsFiddle

Emulieren von privaten Methoden mit Closures

Sprachen wie Java ermöglichen die Deklaration von privaten Methoden, was bedeutet, dass sie nur von anderen Methoden in derselben Klasse aufgerufen werden können.

JavaScript verfügt nativ über kein solches Feature, jedoch ist es möglich, private Methoden mit Closures nachzubilden. Private Methoden sind nicht nur nützlich, um den Zugriff einzuschränken, sodern ermöglichen auch die komfortable Verwaltung des globalen Namensraums, wodurch verhindert wird, dass nicht-essenzielle Methoden das öffentliche Interface zum Code durcheinanderbringen.

Beim nächsten Beispiel definieren wir einige öffentliche Funktionen, die über Closures auf private Funktionen und Variablen zugreifen können. Dieses Konstrukt ist auch als module pattern (Modul-Entwurfsmuster) bekannt.

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }   
})();

alert(Counter.value()); /* Alert-Meldung: 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* Alert-Meldung: 2 */
Counter.decrement();
alert(Counter.value()); /* Alert-Meldung: 1 */

Hier passiert eine ganze Menge. Bei den früheren Beispielen hatte jede Closure ihre eigene seperate Umgebung; bei diesem Beispiel befinden sich die drei Funktionen Counter.increment, Counter.decrement und Counter.value in ein und derselben Umgebung.

Die gemeinsame Umgebung wird im Funktionskörper einer anonymen Funktion erzeugt, welche sofort nach ihrer Definition aufgerufen wird. Die Umgebung enthält zwei private Elemente: eine Variable privateCounter und eine Funktion changeBy. Auf keines dieser beiden Elemente kann von außerhalb der anonymen Funktion zugegriffen werden. Stattdessen müssen zur Veränderung bzw. dem Abruf des Wertes von privateCounter die drei Funktionen benutzt werden, die von der anonymen Funktion zurückgegeben werden.

Diese drei öffentlich zugänglichen Funktionen sind Closures, die sich ihre Umgebung teilen. Dank dem lexikalischen Scoping von JavaScript kann jede dieser Methoden auf die Variable privateCounter und die changeBy-Funktion zugreifen.

Wir haben also eine anonyme Funktion definiert, die einen Zähler erstellt. Diese wird sofort aufgerufen und das Ergebnis der Counter-Variablen zugewiesen. Das Ergebnis könnte man auch in einer extra Variablen speichern und diese für die Erstellung mehrerer Zähler benutzen:

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
alert(Counter1.value()); /* Alerts 0 */
Counter1.increment();
Counter1.increment();
alert(Counter1.value()); /* Alerts 2 */
Counter1.decrement();
alert(Counter1.value()); /* Alerts 1 */
alert(Counter2.value()); /* Alerts 0 */

Die beiden Zähler arbeiten komplett unabhängig voneinander. Die Umgebung der Zähler ist während der Aufrufe der makeCounter()-Funktion jedesmal unterschiedlich und die Closure-Variable privateCounter hält jedesmal eine andere Instanz.

Der Einsatz von Closures auf diese Weise hat viele Vorteile, die normalerweise der objektorientierten Programmierung zugeschrieben werden; insbesondere das "Verstecken" von Daten (data hiding) und die Datenkapselung.

Erzeugung von Closures in Schleifen: Ein häufiger Fehler

Vor der Einführung des let-Schlüsselworts mit JavaScript 1.7 gab es häufig Probleme mit Closures, wenn sie innerhalb einer Schleife erzeugt wurden. Hier ein Beispiel:

<p id="help">Hilfreiche Notizen sollen hier stehen</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Ihre E-Mail-Adresse'},
      {'id': 'name', 'help': 'Vor- und Nachname.'},
      {'id': 'age', 'help': 'Ihr Alter (mindestens 16 Jahre)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp();

Sicht auf jsFiddle

Im Array helpText werden drei Hinweistexte definiert und jeweils über eine ID mit einem Eingabefeld im Dokument verknüpft. Die Schleifen durchläuft diese drei Elemente des Arrays und verbindet jedes mit einem onfocus-Event für das Anzeigen des entsprechenden Hilfetextes.

Probiert man diesen Code aus, wird man feststellen, dass er nicht funktioniert - egal welches Element fokussiert wird, immer erscheint der Text für das Alter.

Der Grund dafür ist, dass die Funktionen, welche onfocus zugewiesen wurden, Closures sind. Sie bestehen aus der Funktionsdefinition und der integrierten Umgebung innerhalb des Sichtbarkeitsbreichs der setupHelp-Funktion. Es wurden zwar drei Closures erstellt, doch alle teilen sich dieselbe Umgebung. Zur Zeit der Ausfühurng der onfucus-Callbacks ist die Schleife bereits durchgelaufen und die item-Variable (die sich alle Cloures teilen) hat den Wert des letzten helpText-Elements.

Eine Lösung für dieses Problem könnte die Benutzung von mehr Closures sein; also eine factory, wie bereits oben beschrieben:

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp();

Sicht auf jsFiddle

Nun funktioniert der Code wie gewünscht - anstatt dass sich die Callbacks alle eine Umgebung teilen, erzeugt die makeHelpCallback-Funktion jeweils eine seperate Umgebung.

Überlegungen zur Performance (Effizienz)

Es ist nicht empfehlenswert, Funktionen ineinander zu verschachteln, wenn Closures zur Erledigung einer bestimmten Aufgabe nicht unbedingt nötig sind, da dies die Performance sowohl hinsichtlich der Ausführungsgeschwindigkeit als auch des Speicherverbrauchs negativ beeinflusst.

Erstellt man beispielsweise ein neues Objekt, sollten Methoden normalerweise mit dem Prototype des Objekts verknüpft und nicht im Konstruktor definiert werden, da sonst die Methoden bei jedem Aufruf des Konstruktors (jeder Erstellung eines Objekts) erneut zugewiesen werden.

Hier ein nicht sehr praxistaugliches aber anschauliches Beispiel:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

Dieser Code zieht keinen Nutzen aus den Vorteilen von Closures und sollte daher wie folgt abgeändert werden:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

Alternativ:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

Bei den beiden letzteren Beispielen teilen sich alle Objekte den Prototype und die Methoden müssen nicht bei jeder Objekterstellung erzeugt werden. Im Kapitel Feinheiten des Objektmodells findet man dazu nähere Informationen.