Zahlreiche Scripte, die JavaScript-Programmierer im Netz anbieten, liegen in einer gesonderten Datei vor und sind darüber hinaus unstrukturiert. Es handelt sich um eine lose Sammlung von dutzenden globalen Variablen und Funktionen:
var variable1 = "wert"; var variable2 = "wert"; var variable3 = "wert"; function funktion1() { /* … */ } function funktion2() { /* … */ } function funktion3() { /* … */ }
Diese Organisation bringt in der Regel mit sich, dass das Script nicht einfach konfigurierbar, anpassbar und erweiterbar ist. Am schwersten wiegt jedoch, dass es sich um eine große Zahl von losen Objekten im globalen Scope (Variablen-Geltungsbereich) handelt. Globale Variablen und Funktionen sind Eigenschaften des window
-Objekts. Das obige Beispiel definiert daher sechs Eigenschaften beim window
-Objekt: window.variable1
bis window.variable3
sowie window.funktion1
bis window.funktion3
.
Unstrukturierte Scripte sind schlecht zu warten und kollidieren mit anderen Scripten. Vermeiden Sie globale Variablen, soweit möglich.
Clientseitige JavaScripte arbeiten unter besonderen Bedingungen: Ein Script operiert auf einem HTML-Dokument, auf das es über das DOM zugreift. Ferner operiert es im Kontext des sogenannten globalen Objekts. Das ist in JavaScript das window
-Objekts, welches den obersten Namensraum bereitstellt. Sowohl das globale Objekt als auch das DOM teilt es sich mit anderen Scripten. Diese »öffentlichen Güter« darf kein Script für sich alleine beanspruchen.
Wenn Scripte unterschiedlicher Herkunft zusammenkommen, kann das schnell zu Konflikten führen. Die Vermeidung von Konflikten setzt bereits beim Unobtrusive JavaScript an: Indem wir JavaScript-Code nicht direkt ins HTML-Dokument einbetten, sondern fortgeschrittenes Event-Handling verwenden, reduzieren wir Überschneidungen im DOM.
Konfliktfeld Nummer Eins bleibt das window
-Objekt. Darüber können über das zwei Scripte zusammenarbeiten, aber auch in Konflikt geraten, wenn sie gleichnamige Variablen definieren. In JavaScript gilt es daher, ein Gleichgewicht zwischen Kapselung und Verfügbarkeit herzustellen.
Datenkapselung bedeutet, dass das Erweitern des globalen Objekts sowie der DOM-Objekte auf ein Minimum reduziert wird. Ein Script sollte den globalen Scope nicht für seine Arbeitsdaten verwenden und globale Variablen möglichst vermeiden. Es sollte nur die Objekte am window
-Objekt speichern, die für den Zugriff von außen unbedingt vonnöten sind.
Die öffentliche API ihres Scriptes benötigt nur ein globales Objekt, über welches die restlichen Funktionen zugänglich sind.
Bei manchen Aufgaben ist es möglich, ein Script konsequent zu kapseln, sodass es das globale window
-Objekt nicht antastet. In anderen Fällen ist es nötig, zumindest einige Objekte global verfügbar zu machen. Gründe dafür können sein:
Es kommt daher auf das richtige Gleichgewicht an. Zwei Beispiele: Das riesige jQuery-Framework definiert standardmäßig nur zwei globale Variablen: window.jQuery
und den Alias window.$
. Das YUI-Framework definiert lediglich window.YUI
. Sowohl window.jQuery
als auch window.YUI
sind Funktionen, denen man beim Aufruf letztlich Funktionen übergibt - dazu später mehr. Beide Frameworks schaffen es, nicht mehr als ein globales Objekt anzulegen, ohne auf die obigen Features wie Erweiterbarkeit zu verzichten.
Object
-Objekte sind besonders vielseitig. Sie sind das Grundwerkzeug zur Gruppierung von Objekten und damit zur Strukturierung von Programmen. Sie sind als Hashes allgegenwärtig.
Eine einfache Möglichkeit, um den globalen Scope zu schonen, ist die Gruppierung aller Variablen und Funktionen eines Scripts in einer JavaScript-Objektstruktur. Im globalen Geltungsbereich taucht dann nur noch diese eine Objektstruktur auf, andere globale Variablen oder Funktionen werden nicht belegt. Das Script ist in der Objektstruktur in sich abgeschlossen. Damit sind Wechselwirkungen mit anderen Scripten ausgeschlossen, solange der Bezeichner der Objektstruktur eindeutig ist.
Ein JavaScript-Objekt ist erst einmal nichts anderes als ein Container für weitere Daten. Ein Objekt ist eine Liste, in der unter einem Bezeichner gewisse Werte gespeichert sind. Aus anderen Programmiersprachen ist diese solche Datenstruktur als Hash oder assoziativer Array bekannt. In JavaScript sind alle vorgegebenen Objekte und Methoden in solchen verschachtelten Objektstrukturen organisiert, z.B. window.document.body
.
In JavaScript gibt es den allgemeinen Objekttyp Object
, von dessen Prototypen alle anderen JavaScript-Objekte abstammen. Das heißt, jedes JavaScript-Objekt ist immer auch ein Object
-Objekt. Object
ist die Grundlage, auf der die restlichen spezifischeren Objekttypen aufbauen.
Für die Organisation von eigenen Scripten bieten sich solche unspezifischen Object
-Objekte an. Über new Object()
lässt sich ein Object
-Objekt erzeugen:
var Modul = new Object(); Modul.eigenschaft = "wert"; Modul.methode = function () { alert("Modul-Eigenschaft: " + Modul.eigenschaft); }; Modul.methode();
Über die gewohnte Schreibweise zum Ansprechen von Unterobjekten (objekt.unterobjekt
) werden dem Object
weitere Objekte angehängt. Im Beispiel werden zwei Objekte angehängt, ein String und eine Funktion.
Der Name Modul
ist selbstverständlich nur als Platzhalter gemeint. Sie sollten das Object
-Objekt (im Folgenden kurz Object
genannt) eindeutig und wiedererkennbar nach der Aufgabe bzw. dem Zweck ihres Scriptes benennen.
Der Object
-Literal erlaubt das kompakte Erzeugen von Object
-Objekten und eignet sich hervorragend für die Definition von Modulen.
JavaScript bietet für das Definieren von Object
-Objekten eine Kurzschreibweise an, den sogenannten Object-Literal. Ein Object
-Literal beginnt mit einer öffnenden geschweiften Klammer {
und endet mit einer schließenden geschweiften Klammer }
. Dazwischen befinden sich, durch Kommas getrennt, die Zuweisungen von Namen zu Objekten. Zwischen Name und Objekt wird ein Doppelpunkt notiert. Das Schema ist also: { name1 : objekt1, name2 : objekt2, … nameN : objektN }
Das obige Beispiel-Object
lässt sich in der Literalschreibweise so umsetzen:
var Modul = { eigenschaft : "wert", methode : function () { alert("Modul-Eigenschaft (über window.Modul): " + Modul.eigenschaft); // Alternativ: alert("Modul-Eigenschaft (über this): " + this.eigenschaft); } }; Modul.methode();
Eine Illustration der entstehenden Verschachtelung:
window
(globales Objekt)
Modul
(Object)
eigenschaft
(String)methode
(Function)Der Zugriff auf die Unterobjekte des Object
-Containers ist von außen über den globale Namen nach dem Schema Modul.eigenschaft
möglich. Im Beispiel wird über Modul.methode()
die zuvor angehängte Funktion aufgerufen.
Wirksame Kapselung erreichen Sie mit einer Funktion, die Ihre Variablen einschließt und nur wenige Objekte nach außen verfügbar macht.
Beim Objekt-Literal wird ein globales Objekt als Namensraum benutzt, um darin eigene Objekte unterzubringen. All diese Objekte sind über das Containerobjekt für andere Scripte zugänglich. Es gibt also keine Trennung zwischen öffentlichen und privaten Daten. Während es sinnvoll ist, dass z.B. eine Methode Modul.methode()
von außen aufrufbar ist, ist es unnötig und potenziell problematisch, dass jede Objekteigenschaft gelesen und manipuliert werden kann.
Der nächste Schritt ist daher, eine wirksame Kapselung zu implementieren. Das Mittel dazu ist ein eigener, privater Scope (Variablen-Geltungsbereich). Darin können beliebig viele lokale Variablen und Methoden definiert werden. Die einzige Möglichkeit, in JavaScript einen Scope zu erzeugen, ist eine Funktion. Wir definieren also eine Funktion, um darin das gesamte Script zu kapseln. Solange durchgehend lokale Variablen und Funktionen verwendet werden, wird der globale Scope nicht angetastet.
Schließen Sie Ihren Code in einen Funktionsausdruck ein, der sofort ausgeführt wird. Darin können Sie mit Objekten quasen, ohne den globalen Scope zu verpesten.
Ein mittlerweile stark verbreitetes Muster ist daher folgender Codeschnipsel:
(function () { /* … */ })();
Dies erscheint zunächst sehr kryptisch, daher eine schrittweise Zerlegung der Syntax:
function () { … }
(function () {})
(function () { … })()
. Die Parameterliste bleibt in diesem Beispiel leer.;
ab.Kurz gesagt handelt es sich um einen sofort ausgeführten Funktionsausdruck. Daher lautet der englische Name für dieses Programmiermuster Immediately-invoked Function Expression, abgekürzt »IIFE«.
Diese anonyme Funktion wird nur notiert, um einen Scope zu erzeugen, und sie wird sofort ausgeführt, ohne dass sie irgendwo gespeichert wird. Innerhalb der Funktion wird nun der gewünschte Code untergebracht:
(function () { /* Lokale Variable */ var variable = 123; /* Lokale Funktion */ function funktion() { /* … */ } /* Rufe lokale Funktion auf: */ funktion(); /* Zugriff auf globale Objekte ist ebenfalls möglich: */ alert(document.title); })();
Im Beispiel finden sich eine Variablendeklarationen und eine Funktionsdeklaration. Beide sind lokal, sind also nur innerhalb der Kapselfunktion zugänglich. Wir können auf die Variablen und Funktionen direkt zugreifen.
Vergessen Sie nicht, Variablen mit var
als lokal zu deklarieren. Andernfalls werden sie automatisch global, also Eigenschaften von window
.
Das Beispiel macht noch nichts sinnvolles. Die Nützlichkeit von Funktionen zur Kapselung ergibt sich z.B. bei einem Anwendungsbeispiel mit Event-Handling.
(function () { var clickNumber = 0; var outputEl; function buttonClicked() { clickNumber++; outputEl.html('Button wurde ' + clickNumber + ' Mal angeklickt'); } function init() { outputEl = jQuery('#output); jQuery('#button').click(buttonClicked); } jQuery(document).ready(init); })();
Das zugehörige HTML:
<button id="button">Klick mich</button> <p id="output">Button wurde noch nicht angeklickt</p> <script src="beispiel.js"></script>
Der Code nutzt die jQuery-Bibliothek, um eine Initialisierungsfunktion bei DOM ready auszuführen. Diese registriert bei einem Button einen Event-Handler. Wird der Button geklickt, wird eine Zahl erhöht. Zudem wird die bisherige Anzahl der Klicks im Dokument ausgegeben.
Das Besondere an diesem Script sind die vier lokalen Variablen bzw. Funktionen. Sie werden direkt im Funktions-Scope notiert, anstatt sie an einen Object
-Container zu hängen. Innerhalb der verschachtelten Funktionen sind die Variablen des äußeren Funktions-Scope verfügbar (siehe Closures). init()
füllt die Variable outputEl
und greift auf die Funktion buttonClicked()
zu. buttonClicked()
greift auf die Variablen clickNumber
und outputEl
zu. Das Script funktioniert, ohne dass Objekte am globalen window
-Objekt angelegt werden.
DOM-Ready-Handler-Funktionen in verschiedenen Bibliotheken bieten berets einen privaten Scope, den Sie nutzen sollten.
Bei der Verwendung mit jQuery ist das Anlegen solcher Funktions-Scopes gang und gäbe. Wenn die Initialisierung eines Scriptes auf DOM ready warten soll, dann übergibt man einen Funktionausdruck an jQuery(…)
. Diese Funktion wird als Handler beim Eintreten des DOM-ready-Ereignisses ausgeführt. Man nutzt sie gleichzeitig als privaten Scope für weitere Objekte. Das obige Beispiel können wir also folgendermaßen anpassen:
jQuery(function ($) { var clickNumber = 0; var outputEl; function buttonClicked() { clickNumber++; outputEl.html('Button wurde ' + clickNumber + ' Mal angeklickt'); } function init() { outputEl = $('#output); $('#button').click(buttonClicked); } init(); });
Die übergebene DOM-ready-Funktion bekommt das globale jQuery-Objekt als ersten Parameter. Wir nennen den Parameter hier $
. Funktionsparameter sind automatisch lokale Variablen, das heißt, wir können mit $
genauso umgehen wie mit clickNumber
oder buttonClicked
.
Das Übergeben von Objekten in die Kapselfunktion verkürzt die Scope-Kette und beschleunigt den Zugriff auf diese Objekte etwas.
jQuery stellt standardmäßig window.$
als Abkürzung für window.jQuery
zur Verfügung, wenn nicht der noConflict-Modus aktiviert wird. Es ergibt jedoch Sinn, das jQuery-Objekt als lokale Variable zu definieren, denn das beschleunigt den Zugriff darauf (Stichwort Scope-Chain).
Aus demselben Grund hat es sich eingebürgert, das window
-Objekt sowie weitere häufig benutzte Objekte wie document
mittels Parametern in den Funktions-Scope zu übergeben:
(function (window, document, undefined) { /* … */ })(window, document);
Gleichzeitig wird hier sichergestellt, dass innerhalb der Funktion der Bezeichner undefined
immer den Typ Undefined
besitzt. Wir definieren einen solchen Parameter, aber übergeben keinen Wert dafür – sodass eine lokale Variable namens undefined
mit einem leeren Wert angelegt wird. Das ist andernfalls nicht garantiert, denn window.undefined
ist durch Scripte überschreibbar.
Innerhalb der Funktion können die Objekte genauso heißen wie außerhalb. Dennoch handelt es z.B. bei document
innerhalb der Funktion um eine lokale Variable, auch wenn sie natürlich auf window.document
verweist.
Wir haben nun beide Extreme kennengelernt: Bei Object
-Containern sind alle Unterobjekte öffentlich. Bei einer Kapselfunktion ist kein Objekt nach außen hin zugänglich. Wenn wir ein wiederverwendbares Script schreiben wollen, wollen wir meist eine öffentliche Programmierschnittstelle (API) anbieten. Dazu müssen einige ausgewählte Objekte, in der Regel Methoden, sowohl nach außen sichtbar sein als auch Zugriff auf die internen, privaten Objekte haben. Man spricht in diesem Fall von privilegierten Methoden.
Das Revealing Module Pattern erlaubt öffentliche und private Objekte und eignet sich ideal, um API und interne Implementierung sauber zu trennen.
Diesen Kompromiss erreichen wir durch eine Kombination aus Object
-Literalen und einer Kapselfunktion. Dieses Entwurfsmuster nennt sich Revealing Module Pattern. Kurz gesagt gibt die Kapselfunktion ein Objekt nach draußen, bevor sie sich beendet. Über dieses Objekt können gewisse privilegierte Methoden aufgerufen werden.
Wir beginnen mit dem bereits beschriebenen Funktionsausdruck, der sofort ausgeführt wird:
(function () { /* … private Objekte … */ })();
Das Neue ist, dass diese Funktion einen Wert zurückgibt, der in einer Variable gespeichert wird:
var Modul = (function () { /* … private Objekte … */ })();
Dieser Wert ist ein Objekt, welches wir in der Funktion mit einem Objekt-Literal notieren und mittels return
nach draußen geben. An dem Objekt hängen die öffentlichen Eigenschaften und Methoden:
var Modul = (function () { /* … private Objekte … */ /* Gebe öffentliche API zurück: */ return { öffentlicheMethode : function () { … } }; })();
Innerhalb der anonymen Funktion notieren wir wie üblich unsere privaten Objekte. Das folgende Beispiel definiert eine öffentliche, privilegierte Methode. Sie hat Zugriff auf sämtliche internen, privaten Objekte, welche direkt von außen nicht zugänglich sind.
var Modul = (function () { // Private Objekte var privateVariable = "privat"; function privateFunktion() { alert("privateFunktion wurde aufgerufen\n" + "Private Variable: " + privateVariable); } // Gebe öffentliches Schnittstellen-Objekt zurück return { öffentlicheMethode : function () { alert("öffentlicheMethode wurde aufgerufen\n" + "Private Variable: " + privateVariable); privateFunktion(); } }; })(); // Rufe öffentliche Methode auf Modul.öffentlicheMethode(); // Ergibt undefined, weil von außen nicht sichtbar: window.alert("Modul.privateFunktion von außerhalb: " + Modul.privateFunktion);
Da die privilegierten Methoden innerhalb des Funktions-Scope notiert werden, haben sie darauf Zugriff. Das liegt daran, dass sie Closures sind.
Module können Sie mit einem Object
in einem Namensraum gruppieren.
Es ist natürlich möglich, solche Module nicht direkt als globale Variablen zu speichern, sondern verschiedene in einem Object
-Literal zu speichern. Dieser dient dann als Namensraum für zusammengehörige Module. So ist letztlich mehrere Module unter nur einer globalen Variable gespeichert.
var Namensraum = {}; Namensraum.Modul1 = (function () { … })(); Namensraum.Modul2 = (function () { … })();
Module nachträglich zu erweitern ist möglich, allerdings haben die einzelnen Teile keinen Zugriff auf die privaten Objekte der anderen Teilmodule.
Ben Cherry schlägt eine Erweiterbarkeit von Modulen auf Basis des Revealing Module Patterns vor. Er unterscheidet zwischen fester und lockerer Kopplung der Teile. Das heißt, entweder setzt ein Aufbaumodul ein Basismodul zwingend voraus. Oder beide Module ergänzen sich gegenseitig, sind aber auch separat funktionsfähig.
/* Grundmodul */ var Modul = (function (Modul) { /* … private Objekte … */ return { methode1 : function () { … } }; })(); /* Erweiterung des Grundmoduls */ (function (modul) { /* … private Objekte … */ /* Erweitere Modul um neue Methoden: */ modul.methode2 = function () { … }; })(Modul);
Die Definition des Grundmoduls erfolgt wie beim Revealing Module Pattern besprochen. Zur Erweiterung des Moduls wird eine weitere anonyme Funktion angelegt und ausgeführt. Diese Funktion bekommt das Modulobjekt als Parameter übergeben und fügt diesem neue Methoden hinzu oder überschreibt vorhandene. Innerhalb der Funktion können wie üblich private Objekte und Methoden angelegt werden.
Nach der Ausführung des obigen Codes besitzt das Modul zwei öffentliche Methoden:
Modul.methode1(); Modul.methode2();
Zu beachten ist, dass die Methoden der Erweiterung keinen Zugriff auf die privaten Objekte des Grundmoduls haben – denn sie befinden sich in einem anderen Funktions-Scope. Zur Lösung dieses Problems schlägt Ben Cherry eine Methode vor, die die privaten Objekte kurzzeitig öffentlich macht, sodass ein übergreifender Zugriff möglich ist. Das erscheint mir jedoch besonders umständlich – in diesem Fall würde ich privaten Objekte zu dauerhaft öffentlichen Eigenschaften machen und auf die vollständige Kapselung verzichten.
Bei der losen Kopplung können die Teilmodule alleine oder zusammen stehen. Ferner ist die Reihenfolge, in der die Teilmodule notiert werden, unwichtig. Dafür können sie nicht stillschweigend auf die gegenseitigen öffentlichen Methoden zugreifen, sondern müssen gegebenenfalls prüfen, ob diese definiert sind.
var Modul = (function (modul) { /* … private Objekte … */ /* Lege Methode am Modulobjekt an: */ modul.methode1 = function () { … }; return modul; }(Modul || {})); var Modul = (function (modul) { /* … private Objekte … */ /* Lege Methode am Modulobjekt an: */ modul.methode2 = function () { … }; return modul; }(Modul || {}));
Die Moduldeklarationen sind gleich aufgebaut: Es gibt eine anonyme Funktion, um einen privaten Scope zu erzeugen. Diese Funktion bekommt das bestehende Modul übergeben. Der Ausdruck Modul || {}
prüft, ob das Modul bereits definiert wurde. Falls ja, wird dieses der Funktion übergeben. Andernfalls wird mit dem Object
-Literal ein leeres Objekt erzeugt und übergeben. Somit ist gesichert, dass die Funktion ein Objekt als Parameter entgegennimmt. Innerhalb der Funktion können wir private Objekte notieren und das Modulobjekt um neue Eigenschaften erweitern. Am Ende wird das Modul zurückgegeben und der Rückgabewert in einer Variable gespeichert.
Das Resultat ist ebenfalls, dass das Modul zwei öffentliche Methoden besitzt:
Modul.methode1(); Modul.methode2();