molily Navigation

Performance von JavaScript-Closures

Closures sind in JavaScript ein mächtiges Werkzeug. Wer die funktionale Natur von JavaScript verstanden hat, wird schnell erfahren, wie Closures die JavaScript-Programmierung vereinfachen und elegantere Lösungswege ermöglichen.

Funktionen in JavaScript

Funktionen als vollwertige Objekte

In JavaScript sind Funktionen vollwertige Objekte (»first-class objects«). Funktionen können zur Laufzeit angelegt, gelöscht, übergeben und mit Unterobjekten erweitert werden. Funktionen haben wie alle Objekte eine Konstruktorfunktion (nämlich Function) und ein prototypisches Objekt (nämlich Function.prototype).

Funktionen erzeugen Scopes

Funktionen sind ferner die einzige Möglichkeit, in JavaScript einen Scope (Variablen-Gültigkeitsbereich) zu erzeugen. Verschachtelt man Funktionen, so entsteht eine Scope-Kette (Scope Chain) mit den internen Variablen-Objekten, an denen die lokalen Funktionsvariablen gespeichert werden. Notiert man einen Bezeichner wie z.B. foo, so wird diese Objekt-Kette von unten bis zum globalen Objekt window durchsucht, um den Bezeichner aufzulösen, also die zugehörige Variable zu finden. Das bietet die Grundlage dafür, dass eine verschachtelte Funktion die Variablen einer äußeren Funktion einschließen und konservieren kann - über den Ablauf der äußeren Funktion hinaus.

Closures und Performance

Ich habe mit Artikeln und Forumsbeiträgen stets versucht, das Wissen über funktionale JavaScript-Programmierung und sinnvolle Anwendungsfälle von Closures zu verbreiten. In letzter Zeit frage ich mich aber zunehmend, ob der Closures-Einsatz nicht überstrapaziert wird nach dem Sprichwort: Beherrscht man einmal den Hammer, so sieht jedes Problem wie ein Nagel aus.

Wenn Funktionen vollwertige individuelle Objekte sind, lokale Variablen konserviert werden und Scope-Ketten durch Verschachtelung lang werden, dann hat das massive Auswirkungen auf die Performance von JavaScripten. Zum einen frisst das Anlegen von zusätzlichen Funktionen Zeit, zum anderen belegt die Persistenz »eingeschlossener« Daten den Arbeitsspeicher. Vielen JavaScript-Programmierern ist nicht bewusst, dass ihre Closures mehr als die von ihnen gewünschten Daten konservieren und sie damit der automatischen Speicherbereinigung des JavaScript-Interpreters, dem Garbage Collector, entziehen.

Closures sowie die damit möglichen zirkulären Referenzen waren in der Vergangenheit Auslöser für Speicherlecks in Browsern. Mein Augenmerk gilt jedoch nicht diesen größtenteils behobenen Fehlern, auch wenn fehlerhafte Browser wie IE 6 und 7 immer noch eine Rolle in der Webentwicklung spielen. Ich möchte Programmiertechniken untersuchen, die sich durch hohe Geschwindigkeit und geringe Speichernutzung in den JavaScript-Engines aktueller Browser auszeichnen.

Erzeugen von Event-Handler-Funktionen

Das Erzeugen von Funktionen zur Laufzeit ist mittlerweile sehr schnell. Totzdem ist ein sparsamer Umgang mit Funktionsobjekten geboten. Besonders beim Event-Handling werden massenhaft Funktionen an DOM-Elemente angehangen. Eine solche häufige Struktur zeigt folgendes Beispiel:

function vegebeHandler () {
	var elements = document.getElementByTagName("p");
	for (var i = 0; l = elements.length; i < l; i++) {
		elements[i].onclick = function () {
			alert("Absatz wurde angeklickt.");
		};
	}
}
window.onload = vegebeHandler;

Aus Performance-Sicht ist folgendes festzustellen:

Langsames Erzeugen von Funktionsobjekten
Wenn das Dokument 100 p-Elemente besitzt, so werden hier 100 neue Funktionsobjekte erzeugt, auch wenn der Funktionskörper gleich ist. Es wird (wenn der Interpreter nicht optimiert) hundertmal soviel Speicher belegt, wie eigentlich nötig wäre.
Closures konservieren Daten, die den Speicher belegen
Noch schwerwiegender ist, dass all diese Funktionen auch Closures darstellen. Sie schließen die Variable elements ein und verhindern die Freigabe des belegten Speichers. elements ist eine »Live«-Knotenliste, die vom Browser immer auf dem aktuellen Stand gehalten werden muss. Daraus folgen meiner Vermutung auch geringe Geschwindigkeitseinbußen bei DOM-Operationen.
Die längere Scope Chain verlangsamt die Auflösung von Bezeichnern

Die Scope Chain der Handler-Funktionen enthält nicht nur den eigenen Funktions-Scope und den globalen Scope, sondern ist durch den von vergebeHandler erweitert. Das verlangsamt die Auflösung von Bezeichnern (Identifier Resolution) und damit die Ausführung der Handler-Funktion. (Siehe: JavaScript variable performance von Nicholas C. Zakas).

Konkret heißt das, dass der Bezeichner »alert« an drei anstatt an zwei Objekten gesucht wird. Fündig wird der Interpreter im globalen Scope, denn mit alert ist freilich window.alert gemeint.

Referenzen statt neue Funktionen

Schon bei diesem einfachen Beispiel sehen wir, dass das gedankenlose Anlegen von Funktionsobjekten mit Performance-Tücken verbunden ist. JavaScript hat zwar dieses tolle Feature, aber wenn wir stattdessen mehrere Referenzen auf ein und dieselbe Funktion anlegen können, so sollten wir diesen Weg wählen.

function handler () {
	alert("Absatz wurde angeklickt.");
}
function vegebeHandler () {
	var elements = document.getElementByTagName("p");
	for (var i = 0, l = elements.length; i < l; i++) {
		elements[i].onclick = handler;
	}
}

Nun will ich mit diesem Beispiel nicht sagen, dass alle Funktionen global sein sollten. Die Handler-Funktion handler könnte beispielsweise an einem Object hängen, das als Object-Literal notiert wurde. Das Beispiel soll nur eine Variante demonstrieren, bei der Funktionen nicht verschachtelt werden und damit die Performance-Nachteile wegfallen.

Unter Laborbedingungen habe ich das Erzeugen von Referenzen auf dieselbe Funktion mit dem Anhängen immer neuer Funktionen an einen Array verglichen.

Browser: Chrome 2.0.172.33, 4.000.000 Iterationen
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
1 Referenzieren 14.148 k 0,003537 k/n 366 ms 0,0915 µs/n
2 Erzeugen 70.216 k 0,017554 k/n
× 4,96
1.811 ms 0,45275 µs/n
× 4,95
Browser: Internet Explorer 8, 400.000 Iterationen
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
1 Referenzieren 35.284 k 0,08821 k/n 588 ms 1,47 µs/n
2 Erzeugen 96.772 k 0,24193 k/n
× 2,74
830 ms 2,075 µs/n
× 1,41

Laufzeit und Speicherverbrauch stehen in keinem Verhältnis.

Wenn Closures nötig scheinen

In diesem einfachen Fall ist es offensichtlich unnötig, die Funktion immer wieder neu innerhalb der Schleife zu erzeugen. Es verschlechtert bloß die Performance, unzählige Closures zu erzeugen.

Spannend wird es den den Fällen, in den man gängige Probleme der Datenverfügbarkeit mit Closures löst. Eine häufig gestellte Frage ist etwa: Eine Funktion berechnet in einer Schleife gewisse Daten und registriert einen Event-Handler. In dieser Handler-Funktion sollen nun die gerade in diesem Schleifendurchlauf berechneten Daten zur Verfügung stehen - und sei es nur die Laufvariable der Schleife.

Eine Standardlösung verwendet in jedem Schleifendurchlauf 1.) einen Funktionsausdruck zum notieren einer anonymen Funktion, die sofort ausgeführt wird, und 2.) eine Closure, die den Scope der äußeren anonymen Funktion einschließt.

function vegebeHandler () {
	var elements = document.getElementByTagName("p");
	for (var i = 0; l = elements.length; i < l; i++) {
		/* Funktionsausdruck (anonyme Funktion): */
		(function (i) {
			/* Closure: */
			elements[i].onclick = function () {
				alert("Absatz Nummer " + i + " wurde angeklickt.");
			};
		})(i);
		/* Sofortige Ausführung der anonymen Funktion, übergeben des Schleifenzählers */
	}
}

Performance-Profil:

Erzeugung von Funktionen, das Variablen-Objekt bleibt erhalten
Bei 100 p-Elementen werden 100 anonyme Funktionen erzeugt, ausgeführt und sofort wieder automatisch gelöscht, da sie nirgends gespeichert werden. Der Garbage Collector räumt zwar die Funktion weg, allerdings bleibt das Variablen-Objekt mit der Variable i erhalten.
Erzeugung der Closure-Funktionen
Bei 100 p-Elementen werden 100 individuelle Handler-Funktionen erzeugt.
Vier Objekte in der Scope Chain
Der Scope einer Handler-Funktion enthält vier Variablen-Objekte, an denen ein Bezeichner gesucht wird. Der Zugriff auf »i« ist schnell, der Zugriff auf »alert« ist langsam.

Unter Laborbedingungen habe ich Closures erzeugt und wiederum an Arrays gehängt anstatt an DOM-Elemente. Da der Knackpunkt beim Anlegen von Closures gerade darin besteht, neue Funktionen zu erzeugen, dient als Vergleichsfall das zweite Beispiel, welches bei jedem Array-Durchlauf eine Closure erzeugt, aber keine anonyme Funktion für den Einschluss von i erzeugt und ausführt.

Browser: Chrome 2.0.172.33, 4.000.000 Iterationen
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
2 Erzeugen 70216 k 0,017554 k/n 1811 ms 0,45275 µs/n
3 Closures 75344 k 0,018836 k/n
× 1,073
3929 ms 0,98225 µs/n
× 2,17

Die Speicherung der Variablen-Objekte führt nur zu einer vergleichsweise kleinen Steigerung des Speicherverbrauchs. Das Anlegen und Ausführen der anonymen Funktionen hingegen macht das Unternehmen gleich mehr als doppelt so langsam.

Chromes V8 ist eine weit entwickelte und hochoptimierte JavaScript-Engine, deren würdige Konkurrenten derzeit Safaris Squirrelfish (auch »Nitro« oder »Squirrelfish Extreme« genannt) und Firefox’ TraceMonkey sind. Den Labortest mit 4 Millionen Durchläufen meistert V9 in nicht einmal vier Sekunden und verbraucht dabei für heutige Rechnerkapazitäten relativ wenig Arbeitsspeicher.

Im Internet Explorer 8 stellt sich ein ganz anderes Bild dar:

Browser: Internet Explorer 8, 400.000 Iterationen
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
2 Erzeugen 96.772 k 0,24193 k/n 830 ms 2,075 µs/n
3 Closures 341.828 k 0,85457 k/n
× 3,53
× 45,4 ggü. Chrome
2.298 ms 5,745 µs/n
× 2,77
× 12,7 ggü. Chrome

Da er fast 13 Mal soviel Zeit benötigt und 45 Mal soviel Speicher belegt, testen wir nur 400.000 Durchläufe. Es ist unglaublich, wie man mit Funktionen und Closures den Arbeitsspeicher in wenigen Sekunden belegen kann.

In der Praxis enthält ein Dokument selten mehr als tausend Elemente zur gleichen Zeit. Insofern sagt dieser Labortest wenig darüber aus, ob es zu massivem Speicherverbrauch in JavaScript-Webanwendungen kommt. Aber das ist auch nicht Zweck des Tests - vielmehr soll der Zuwachs gegenüber Lösungen ohne Closures gezeigt werden.

Kontrolliertes Anlegen von Closures: Currying und Binding

JavaScript-Bibliotheken wie Prototype und Mootools erleichtern die JavaScript-Programmierung, indem sie Funktionsobjekte protoypisch erweitern und mit Methoden versehen, die Currying und Binding erlauben.

Currying ist das geordnete Anlegen von Closures, die gewisse Parameter einschließen und sie an die Ursprungsfunktion übergeben. So wird eine neue Funktion erzeugt, die die Ursprungsfunktion mit vorbelegten Parametern aufruft und ihre Parameter hinzufügt und weitergibt. Mit Binding kommt die Möglichkeit hinzu, die Ursprungsfunktion in Kontext eines Objektes auszuführen, damit »this« in der Funktion auf das gewünschte Objekt zeigt.

Currying und Binding sind in Prototype über curry bzw. bind möglich. In Mootools gibt es dafür create und ebenfalls bind. Speziell für den Einsatz bei Event-Handlern existiert bei Prototype bindAsEventListener und bei Mootools bindWithEvent. All diese sind durch prototypische Erweiterung Methoden von beliebigen Funktionsobjekten. Wir erinnern uns: Funktionen sind Objekte erster Klasse, daher geht das in JavaScript.

bind und bindAsEventListener habe ich auch unter Alternativlösungen zur Kontext-Problematik: bind und bindAsEventListener beschrieben.

Function.prototype.bind wird ECMAScript 5 (Abschnitt 15.3.4.5) standardisiert werden. Allerdings gibt es noch keinen mir bekannten Browser, der bind nativ unterstützt, und man ist derzeit auf die Erweiterung des Function-Prototyps angewiesen. Sobald eine native Implementierung existiert, ist mit einer enorm verbesserten Performance zu rechnen.

Mit Currying können wir das Beispiel eleganter und funktionaler lösen:

function handler (i) {
	alert("Absatz Nummer " + i + " wurde angeklickt");
}
function vegebeHandler () {
	var elements = document.getElementByTagName("p");
	for (var i = 0, element; l = elements.length; i < l; i++) {
		element[i].onclick = handler.curry(i);
	}
}

Wie gesagt wäre hier bindAsEventListener bzw. bindWithEvent angebracht, damit die Handler-Funktion das Event-Objekt browserübergreifend als ersten Parameter übergeben bekommt. Hier geht es jedoch nur um die Weitergabe des korrekten Wertes der Variable i.

In meinem Labortest speichere ich die von curry erzeugen Closures in einem Array. Mit Erschrecken musste ich feststellen, dass diese Variante unverhältnismäßig langsamer ist als alle bisher diskutierten. Ich habe daher nur die Hälfte der Iterationen verwendet:

Browser: Chrome 2.0.172.33
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
3 Closures
4.000.000 Iterationen
75.344 k 0,018836 k/n 3929 ms 0,98225 µs/n
4 Currying
2.000.000 Iterationen
174.612 k 0,087306 k/n
× 4,6
11433 ms 5,7165 µs/n
× 5,8
Browser: Internet Explorer 8
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
3 Closures
400.000 Iterationen
341828 k 0,85457 k/n 2298 ms 5,745 µs/n
4 Currying
200.000 Iterationen
331120 k 1,6556 k/n
× 0,97
2186 ms 10,93 µs/n
× 1,9

Pro Iteration ist das Currying fast sechs Mal (Chrome) bzw. fast doppelt so langsam (IE). Warum, erklärt sich sehr schnell. Die Standard-Currying-Funktionen müssen wegen einem »Bad Part« (Douglas Crockford) in der ECMAScript-Spezifikation die Parameter-Listen (arguments) von Hand in Arrays umwandeln. Die curry-Funktion hat in der Regel eine solche oder ähnliche Struktur:

Function.prototype.curry = (function () {
	var slice = Array.prototype.slice;
	return function () {
		var func = this, args = slice.apply(arguments);
		return function () {
			return func.apply(this, args.concat(slice.apply(arguments)));
		};
	};
})();

Hier wird zweimal Array.prototype.slice für die arguments-Listen aufgerufen, um sie in normale JavaScript-Arrays umzuwandeln. Die Geschwindigkeitseinbuße rührt zum großen Teil von diesen slice-Aufrufen her, die mit dem eigentlichen Anlegen der Closures nichts zu tun haben.

Leider bleibt einer praxistauglichen, flexiblen Currying-Funktion nichts anderes übrig, als solche Umwandlungen vorzunehmen. Juriy Zaytsev hat im Ticketsystem für Prototype eine hochoptimierte Variante von Function.prototype.bind vorgeschlagen, die das Anlegen neuer Funktionen und das Aufrufen von slice verringert. (Nachtrag: Dieser Patch wurde in Prototype 1.6.1 aufgenommen.) Eine weitere Möglichkeit wäre ein Memoizer, der eine Funktion nur einmal an ein Objekt bindet und diese Closures zwischenspeichert (bspw. Leak Free Javascript Closures).

Vereinfachtes, Performance-optimiertes Currying

Unter Berücksichtigung dieser Ideen habe ich eine vereinfachte, auf den Testfall zugeschnittene und sehr rudimentäre Currying-Funktion hinsichtlich Performance getestet. Sie nimmt nur ein Parameter entgegen und gibt nur diesen Parameter weiter. Array.prototype.slice kann dadurch gänzlich vermieden werden.

Function.prototype.simpleCurry = function (arg) {
	var func = this;
	return function () {
		return func.call(this, arg);
	};
};

Ein Vergleich zwischen dieser stark reduzierten Currying-Funktion und dem umständlichen Anlegen der Closures von Hand ist schon eher möglich:

Browser: Chrome 2.0.172.33
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
3 Closures
4.000.000 Iterationen
75344 k 0,018836 k/n 3929 ms 0,98225 µs/n
5 Simple Currying
2.000.000 Iterationen
77244 k 0,038622 k/n
× 2,05
2870 ms 1,435 µs/n
× 1,46

Auch das zugeschnittene zentrale Anlegen von Closures ist im Chrome signifikant langsamer und verbraucht doppelt soviel Speicher. Dies ist ein Hinweis darauf, dass das »händische« Anlegen von Closures unter Umständen hinsichtlich Performance besser sein kann.

Browser: Internet Explorer 8
Testfall Speicherverbrauch Speicherverbrauch
pro Iteration
Gesamtlaufzeit Laufzeit
pro Iteration
3 Closures
400.000 Iterationen
341.828 k 0,85457 k/n 2.298 ms 5,745 µs/n
5 Simple Currying
200.000 Iterationen
172.592 k 0,86296 k/n
× 1,00
940 ms 4,7 µs/n
× 0,82

Das zentralisierte Anlegen von Closures ist im IE etwas schneller, der Speicherverbrauch bleibt jedoch gleich schlecht.

Performance-Profil dieses Ansatzes:

  • Bei 100 p-Elementen werden wird 100 mal curry ausgeführt. Die Variablen-Objekte dieser Auführungen bleiben im Speicher erhalten.
  • Bei 100 p-Elementen werden 100 Closures erzeugt.
  • Die Scope Chain enthält ebenfalls vier Objekte:

    1. das Variablen-Objekt der Handler-Funktion handler selbst
    2. das Variablen-Objekt der Closure, die in simpleCurry angelegt wurde
    3. das Variablen-Objekt der Helfer-Funktion simpleCurry
    4. schließlich das globale Objekt window.

    Das bedeutet, der Zugriff auf die Variable »i« ist schnell, denn sie wird schon beim ersten Objekt in der Scope Chain gefunden. Der Zugriff auf die Methode »alert« ist verlangsamt, denn sie wird erst an vierter Stelle gefunden.

Meine Schlussfolgerung anhand dieser Labortests lautet: Das kontrollierte Anlegen von Closures macht den Code übersichtlicher und eindeutiger, jedoch sind die üblichen Currying-Methoden zu allgemein, als dass sie hinsichtlich Performance mit einer Low-Level-Umsetzung mithalten könnten.

Vermeidung von Closures – Verfügbarkeit von Daten

Des Lerneffektes halber habe ich ein wenig gemogelt, indem ich ein simples Beispiel aus dem Event-Handling gewählt habe, das üblicherweise mit Closures umgesetzt wird, aber auch hervorragend ohne gelöst werden kann.

Wenn es nämlich bloß darum geht, dass eine Handler-Funktion Zugriff auf gewisse Kontextinformationen bekommt, so besteht die Möglichkeit, diese Informationen im Dokument selbst in Form von Eigenschaften der jeweiligen Elementobjekte zu speichern. Die DOM-Knotenobjekte sind beliebig mit eigenen Unterobjekten erweiterbar.

function handler () {
	alert("Absatz Nummer " + element.__absatznummer + " wurde angeklickt");
	/* Stoppe Event-Handling bei diesem Element */
	this.onclick = undefined;
	delete element.__absatznummer;
}
function vegebeHandler () {
	var elements = document.getElementByTagName("p");
	for (var i = 0, element; element = elements[i]; i++) {
		element.onclick = handler;
		element.__absatznummer = i;
	}
}

Um den jeweiligen Wert der Variable i der Handler-Funktion bereitzustellen, wird er in der Eigenschaft __absatznummer des Elementobjektes gespeichert. In der Handler-Funktion wird diese Information genutzt. Gleichzeitig wird demonstriert, wie die Eigenschaft nach der Nutzung mit delete entfernt werden kann.

Wenn das Element z.B. Teil eines Widgets ist, kann eine Referenz auf das Widget-Objekt angelegt werden. Zur Sicherheit lässt sich ein eigener Namespace am Element anlegen, sodass sich verschiedene Scripte, die Kontextdaten speichern, sich nicht in die Quere kommen.

Manche JavaScript-Frameworks bieten für das Verknüpfen von JavaScript-Werten mit Elementen eine einheitliche API an. Beispielsweise jQuery bietet die data-Methode, um Daten zu einem oder mehreren Elementen zu speichern. (Nachtrag: Prototype 1.6.1 bietet eine gleichwertige Funktionalität über die Methoden store() und retrieve().) Die Werte werden nicht wirklich am Elementobjekt gespeichert, sondern in dem Hash jQuery.cache. Die Daten darin werden über eine eindeutige Nummer (UUID) mit dem Element verknüpft. jQuery.cache benutzt jQuery auch dazu, um registrierte Event-Handler mit einem Element zu verknüpfen.

Der Vorteil dieser Variante, die auf das direkte Verknüpfen von Elementobjekten mit eigenen JavaScript-Objekten verzichtet, ist die Verhinderung von Speicherlecks in Browsern mit fehlerhaftem Garbage Collector – namentlich der Internet Explorer. Der Nachteil ist, dass der Cache-Eintrag für ein Element meines Wissens bestehen bleibt, wenn das Element nicht mit jQuery-eigenen Methoden gelöscht wurde. jQuery-Methoden wie html() überschreiben daher nicht einfach innerHTML-Eigenschaft, sondern entfernen vorher alle Nachfahrenelemente rekursiv und löschen zugehörige Cache-Einträge. Das ist mit entsprechendem Aufwand verbunden, garantiert aber einen kleinstmöglichen Speicherverbrauch.

Mit Referenzen bei DOM-Elementen auf Kontextdaten lassen sich natürlich keinesfalls alle nützlichen Closures ersetzen - nicht einmal im Event-Handling. Dies soll lediglich ein Beispiel dafür sein, den Funktionen- und Closures-Gebrauch zu reflektieren und nach Lösungen zu suchen, die weniger komplex sind, daher schneller laufen und weniger Speicher verbrauchen.

Kapselung durch Closures bei Modulen (Private Member)

An anderen Stellen der JavaScript-Programmierung sind Closures unerlässlich. Da JavaScript nicht über das Konzept von Klassen und einer Sichtbarkeit von Membern verfügt, werden private Member mit Closures emuliert. In diesen Fällen sollte man sich der Auswirkungen auf die Performance zumindest bewusst sein. Benchmarking kann im Einzelfall zeigen, ob eine effektive Kapselung die Performance merklich verschlechtert.

Das Module-Pattern ist eine prominente Anwendung des Konzept von Closures zur Kapselung. Es erzeugt einen Funktions-Scope und notiert darin verschiedene lokale Objekte und Funktionen. Die verschachtelten Funktionen haben Zugriff auf die restlichen Objekte im selben Scope. Schließlich gibt die äußere Wrapper-Funktion ein Objekt zurück, das einige der »privaten« Eigenschaften und Methoden »öffentlich« macht.

MeinModul = (function () { // anonyme Funktion, die sofort ausgeführt wird
	var privateEigenschaft = 123;
	function privateMethode () {
		alert(privateEigenschaft);
	}
	function priviligierteMethode () {
		privateMethode();
	}
	return {
		priviligierteMethode : priviligierteMethode
	};
})();
MeinModul.priviligierteMethode();

(Das Beispiel zeigt das Revealing Module Pattern.)

Closures mit diesem Zweck haben ein großes Potenzial. Sie können immer verwendet werden, wenn Daten zwischen verschiedenen »öffentlichen« Funktionen geteilt werden sollen, ohne dass diese Daten selbst öffentlich sein oder sogar direkt im globalen Scope liegen müssen. Prinzipiell eignet sich diese Technik hervorragend dazu, die Performance von Scripten zu verbessern, weil man mehrfach genutzte Objekte einmal anlegen, verschiedenen Funktionen ohne Umwege bereitstellen kann und somit später vielfach nutzen kann.

Kapselung durch Closures in Konstruktoren

Beim Module Pattern wird eine Wrapper-Funktion erzeugt und nur ein »öffentliches«Objekt angelegt, dessen Closure-Funktionen gewisse »private« Objekte einschließen. Die Anzahl der Closures sowie der Umfang der eingeschlossenen Daten bleiben dabei noch überschaubar. Problematischer wird es, wenn private Member sowie öffentliche Methoden komplett in einem Konstruktor notiert werden, der mehrere Instanzen erzeugt:

function Konstruktor () {
	var privateEigenschaft = new Date().getTime();
	function privateMethode () {
		alert(privateEigenschaft);
	}
	this.priviligierteMethode = function () {
		privateMethode();
	};
}
var instanz = new Konstruktor;
instanz.priviligierteMethode();

Obwohl privateMethode und priviligierteMethode bei verschiedenen Instanzen gleich sind, werden für jede Instanz neue Funktionsobjekte angelegt, denn sie sollen als Closures wirken. Um private Member zu bekommen, die nicht zwischen Objektinstanzen geteilt werden, zahlen wir einen hohen Performance-Preis im Vergleich zur klassischen Methode, die alle Member über das prototype-Objekt definiert. Diese sind dann notwendig öffentlich:

function Konstruktor () {
	this.öffentlicheEigenschaft = new Date().getTime();
}
Konstruktor.prototype = {
	öffentlicheMethode : function () {
		alert(this.öffentlicheEigenschaft);
	}
};
var instanz = new Konstruktor;
instanz.öffentlicheMethode();

Alle Instanzen teilen hier dasselbe Funktionsobjekt. Sie besitzen Referenzen auf die öffentlichen Methoden, beim Erzeugen werden keine neuen Funktionsobjekte erzeugt.

Die Wahl der geeigneten Technik hängt von der benötigten Funktionalität ab. Die unterschiedliche Performance zeigt sich vermutlich nur dann, wenn besonders viele Instanzen erzeugt werden oder der Konstruktor besonders viele Objekte erzeugt. Auf Praxiserfahrungen und Benchmarks konkreter Anwendungsfälle kann ich hier nicht zurückgreifen. Ich persönlich halte private Member bei Objekten, die durch Konstruktoren erzeugt werden, in vielen Fällen für verzichtbar. Zur besseren Strukturierung von Membern reichen mir Präfixe wie »_« für private Member. Die effektive Kapselung, d.h. die unüberwindbare Trennung zwischen öffentlicher API und internen Objekten, erscheint mir nur selten vonnöten.

Matt Snider hat darauf hingewiesen, dass der Konstruktor-Ansatz mit privaten Membern, wie er bei der Programmierung mit der Bibliothek Yahoo! UI (YUI) verwendet wird, immer noch schneller ist als das Klassen-Konzept von Prototype (Class.create), welches mit prototypischer Vererbung anstatt dem Anlegen immer neuer Funktionen für jede Instanz arbeitet. Das liegt vermutlich daran, dass der »Overhead« von Prototype an dieser Stelle sehr groß ist im Vergleich zur Low-Level-Lösung von YUI - wir kennen dieses Phänomen von bind und curry.

Anmerkungen zur Testmethodik

Das Testsystem für die Benchmarks war ein Intel Core 2 Duo T5550 mit 1,83 GHz, 2 GB RAM, Windows Vista. Chrome und IE 8 wurden gewählt, weil sie für jeden Tab in einem eigenen Browser-Prozess starten und somit eine genaue Angabe des Speicherverbrauchs möglich ist. Die Zahlen für Chrome stammen aus about:memory, die für IE 8 aus dem Windows Task-Manager (jeweils totaler Speicherverbrauch).