JavaScript/Module und Kapselung

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Inhaltsverzeichnis

Zahlreiche Scripte, die JavaScript-Programmierer im Netz anbieten, liegen in gesonderten Dateien vor und sind darüber hinaus unstrukturiert. Es handelt sich um eine lose Sammlung von dutzenden globalen Variablen und Funktionen:

Beispiel
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-Gültigkeitsbereich) handelt. Globale Variablen und Funktionen sind Eigenschaften des window-Objektes. Das obige Beispiel definiert daher sechs Eigenschaften beim window-Objekt: window.variable1 bis window.variable3 sowie window.funktion1 bis window.funktion3.


[Bearbeiten] Das Yin und Yang von JavaScript: Kapselung vs. Verfügbarkeit

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 Objektes. Das ist in JavaScript das window-Objekt, 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 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.

Information

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:

  • Eine öffentliche Programmierschnittstelle (API) besonders bei Bibliotheken
  • Konfigurierbarkeit des Scripts z.B. durch verschiedene Nutzer
  • Erweiterbarkeit (Modularisierung)

Es kommt daher auf das richtige Gleichgewicht an. Zwei Beispiele: Das riesige jQuery-Framework definiert standardmäßig nur zwei globale Variablen: window.jQuery() und als Alias window.$(). Das YUI-Framework definiert lediglich window.YUI(). window.jQuery() und window.YUI() sind beides 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.

Empfehlung: Unstrukturierte Scripte sind schlecht zu warten und kollidieren mit anderen Scripten. Vermeiden Sie globale Variablen, soweit möglich.

[Bearbeiten] Einfache Module mit dem Objekt-Literal

Information

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 Unterobjekte gespeichert sind. Aus anderen Programmiersprachen ist eine 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:

Beispiel
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.

Information

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:

Beispiel
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.

[Bearbeiten] Kapselung mit privatem Funktions-Scope

Information

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-Gültigkeitsbereich). 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.

Ein mittlerweile stark verbreitetes Muster ist daher folgender Codeschnipsel:

Beispiel
(function () {
    /* ... */
})();

Information

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.

Dies erscheint zunächst sehr kryptisch, daher eine schrittweise Zerlegung der Syntax:

  1. Erzeuge eine namenlose Funktion per Funktionsausdruck: function () { ... }
  2. Umschließe diesen Funktionsausdruck mit runden Klammern: (function () {})
  3. Führe die Funktion sofort aus mit dem Call-Operator, das sind die beiden runden Klammern: (function () { ... })(). Die Parameterliste bleibt in diesem Beispiel leer.
  4. Schließe die Anweisung mit einem ; ab.


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:

Beispiel
(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.

Empfehlung: 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.

Beispiel
(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örende HTML:

Beispiel
<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.


[Bearbeiten] DOM-ready-Handler als privater Scope

Information

DOM-Ready-Handlerfunktionen in verschiedenen Bibliotheken bieten bereits einen privaten Scope, den Sie nutzen sollten.

Bei der Verwendung mit jQuery ist das Anlegen solcher Funktions-Scope gang und gäbe. Wenn die Initialisierung eines Scriptes auf DOM ready warten soll, dann übergibt man einen Funktionausdruck an jQuery(document).ready(). 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:

Beispiel
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.

[Bearbeiten] Globale Objekte importieren

Information

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:

Beispiel
(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.

[Bearbeiten] Das Revealing Module Pattern: Kapselung plus öffentliche Schnittstelle

Information

Das Revealing Module Pattern erlaubt öffentliche und private Objekte und eignet sich ideal, um API und interne Implementierung sauber zu trennen.

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 Schnittstelle (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.

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:

Beispiel
(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:

Beispiel
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.

Beispiel
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);

Information

Module können Sie mit einem Object in einem Namensraum gruppieren.

Da die privilegierten Methoden innerhalb des Funktions-Scope notiert werden, haben sie darauf Zugriff. Das liegt daran, dass sie Closures sind.

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.

Beispiel
var Namensraum = {};
Namensraum.Modul1 = (function () { ... })();
Namensraum.Modul2 = (function () { ... })();

[Bearbeiten] Erweiterbare Module

Information

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[1] . 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.


[Bearbeiten] Feste Kopplung

Beispiel
/* 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:

Beispiel
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.


[Bearbeiten] Lose Kopplung

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.

Beispiel
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:

Beispiel
Modul.methode1();
Modul.methode2();

[Bearbeiten] Quellen

Information

Bei diesem Text handelt es sich um eine überarbeitete Übernahme aus der Einführung in JavaScript von Mathias Schäfer.[2]
  1. Ben Cherry: JavaScript Module Pattern: In-Depth
  2. Mathias Schäfer: Organisation von JavaScripten: Module und Kapselung


Meine Werkzeuge
Namensräume

Varianten
Aktionen
Übersicht
Index
Mitmachen
Werkzeuge
Spenden
SELFHTML