Teil von SELFHTML aktuell Teil von Artikel Teil von JavaScript

Organisation von JavaScripten

nach unten Mathias Schäfer
nach unten Das Schichtenmodell: Trennung von Inhaltsstruktur, Präsentation und Verhalten
nach unten Warum sind geordnete und strukturierte Scripte sinnvoll?
nach unten Unstrukturierte Scripte
nach unten Objektstrukturen mit Object
nach unten Object-Literale
nach unten Object-Methoden in anderen Kontexten ausführen
nach unten Möglichkeiten der Datenspeicherung
nach unten Eigene Objekte
nach unten Methoden eigener Objekte in anderen Kontexten ausführen
nach unten Einführung in Closures
nach unten Closures zur Kapselung bei Object und eigenen Objekten
nach unten Alternativlösungen zur Kontext-Problematik: bind() und bindAsEventListener()
nach unten Modularisierung und Namensräume
nach unten Ausblick auf JavaScript-Frameworks
nach unten Literaturhinweise

Mathias Schäfer

E-Mail: E-Mail molily@selfhtml.org

Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!

nach obennach unten

Das Schichtenmodell: Trennung von Inhaltsstruktur, Präsentation und Verhalten

Im modernen Webdesign kommt den Webtechniken HTML, CSS und JavaScript jeweils eine bestimmte Rolle zu. HTML soll die Texte sinn- und bedeutungsvoll strukturieren, indem z.B. Überschriften, Listen, Absätze, Datentabellen, zusammenhängende Bereiche sowie wichtige Abschnitte, Zitate usw. als solche ausgezeichnet werden. CSS ist dafür da, die Regeln für Darstellung dieser Inhalte vorzugeben, sei es auf einem Desktop-Bildschirm, auf einem Handheld, auf Papier oder anders.

Um eine Website möglichst effizient und einfach zu entwickeln sowie sie nachträglich mit geringem Aufwand pflegen zu können, sollen diese beiden Aufgaben strikt voneinander getrennt werden: Im HTML-Code werden keine Angaben zur Präsentation gemacht. Im Stylesheet befinden sich demnach alle Angaben zur Präsentation in möglichst effizienter Weise. Dadurch müssen im HTML-Code nur genau soviele Angriffspunkte für CSS-Selektoren gesetzt werden, wie gerade nötig (z.B. zusätzliche div- oder span-Elemente sowie id- und class-Attribute). Ein und dasselbe Dokument kann auf diese Weise durch den Wechsel des Stylesheets ein völlig anderes Layout bekommen. Aber auch ganz ohne Stylesheet sind die Inhalte noch sinnvoll strukturiert und die Inhalte zugänglich.

JavaScript kommt in diesem Konzept die Aufgabe zu, dem Dokument »Verhalten« (Behaviour) hinzuzufügen. Damit ist gemeint, dass das Dokument auf gewisse Anwenderereignisse reagiert und z.B. Änderungen im Dokument vornimmt. Diese Interaktivität wird dem Dokument automatisch hinzugefügt – im HTML-Code sollte sich kein JavaScript in Form von Event-Handler-Attributen befinden (onload, onclick, onmouseover usw.). Stattdessen werden Elemente, denen ein bestimmtes Verhalten hinzugefügt werden soll, z.B. mit einer Klasse markiert. Zeitgemäße Scripte werden automatisch beim Ladens des Dokuments aktiv und starten die Ereignisüberwachung an den betreffenden Elementen. Diese Anwendung von JavaScript nennt sich Seite Unobtrusive JavaScript, »unaufdringliches« JavaScript, oder auch Seite DOM Scripting.

nach obennach unten

Warum sind geordnete und strukturierte Scripte sinnvoll?

Sobald eine Webseite mittels »unaufdringlichem« JavaScript aufgewertet und interaktiver gestaltet wird, entstehen komplexe Scripte. Der Ablauf des Scripts wird in Teilaufgaben geteilt, die verschiedene Funktionen übernehmen. Anstatt ein und denselben Code zu wiederholen, wird er in eine Funktion ausgelagert, die Parameter entgegennehmen kann. Insbesondere das Event-Handling erfordert verschiedene Funktionen, die als Event-Handler dienen oder bei der Verarbeitung von Events helfen.

Um bestimmte Funktionalität umzusetzen, werden meist unzählige Variablen und mehrere Funktionen benutzt – ein zusammenhängendes Script, deren Teile miteinander arbeiten. In ein Dokument werden gerne verschiedene »unaufdringliche« Scripte verschiedenen Ursprungs eingebunden. Sie sollen unabhängig voneinander arbeiten, aber auch reibungslos miteinander funktionieren. So stellen sich folgende Fragen:

Diese Fragen sind inbesondere dann wichtig, wenn mehrere Personen an einem Script arbeiten, wenn ein Script für andere Webautoren veröffentlicht werden soll oder wenn man selbst sein eigenes Script auch nach einiger Zeit wieder verstehen will.

nach obennach unten

Unstrukturierte Scripte

Die meisten Scripte, die JavaScript-Programmierer im Netz anbieten, liegen in einer gesonderten Datei vor und sind darüber hinaus unstrukturiert. Es handelt sich um eine äußerlich 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. In den wenigsten Fällen sind diese Scripte »unaufdringlich«, sie fördern die Vermischung von HTML, CSS und JavaScript. Sie enthalten einerseits selbst »hartkodierten«, das heißt fest eingebundenen HTML- und CSS-Code und erfordern andererseits große Änderungen im HTML-Dokument.

Manche Scripte sind durch Konfigurationsvariablen anpassbar, die vor den tatsächlichen Code gesetzt werden. Ein Seitenautor, der ein fremdes Script in seine Seite einbaut, kann auf diese Weise auch ohne Kenntnis des Scriptes dessen Verhalten ändern.

/* Konfigurationsvariablen */
var konfiguration1 = "anpassbar";
var konfiguration2 = "anpassbar";
var konfiguration3 = "anpassbar";

/* Der folgenden Code sollte unverändert bleiben: */
var variable1 = "wert";
var variable2 = "wert";
var variable3 = "wert";
function funktion1 () { /* ... */ }
function funktion2 () { /* ... */ }
function funktion3 () { /* ... */ }

Wird nun ein weiteres Script eingebunden, so ist die Wahrscheinlichkeit hoch, dass es ähnliche Namen für die Variablen und Funktionen verwendet. In diesem Fall kommt es zu unerwünschten Wechselwirkungen, wodurch die beteiligten Scripte nicht mehr ordnungsgemäß funktionieren. Viele Script-Autoren versehen daher alle Bezeichner mit einem Präfix, der die Zugehörigkeit zu einem bestimmten Script verdeutlicht:

/* Konfigurationsvariablen */
var präfix_konfiguration1 = "anpassbar";
var präfix_konfiguration2 = "anpassbar";
var präfix_konfiguration3 = "anpassbar";

/* Der folgenden Code sollte unverändert bleiben: */
var präfix_variable1 = "wert";
var präfix_variable2 = "wert";
var präfix_variable3 = "wert";
function präfix_funktion1 () { /* ... */ }
function präfix_funktion2 () { /* ... */ }
function präfix_funktion3 () { /* ... */ }

Damit sind zwar schon wichtige Grundlagen geschaffen, allerdings handelt es sich immer noch um eine Zahl von losen Objekten im globalen Geltungsbereich (Scope).

nach obennach unten

Objektstrukturen mit Object

Sinnvoller ist es, alle Objekte – Variablen und Funktionen sind nichts anderes als Objekte – eines Scripts in einer echten JavaScript-Objektstruktur zu gruppieren. Im globalen Geltungsbereich taucht direkt dann nur noch diese Objektstruktur auf, andere globale Variablen oder Funktionen gibt es nicht. Damit sind unerwünschte Wechselwirkungen mit anderen Scripten ausgeschlossen, solange der Bezeichner der Objektstruktur eindeutig ist.

Allgemeines zu Objekten

Ein JavaScript-Objekt ist erst einmal nichts anderes als ein Container für weitere Daten. Ein Objekt ist eine Liste, in dem unter bestimmten Namen gewisse Unterobjekte (auch Member genannt) 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.

Object-Objekte

In JavaScript gibt es den allgemeinen Objekttyp Object. Vom Object-Prototypen stammen alle anderen JavaScript-Objekte ab. Für die Organisation von eigenen Scripten bieten sich Object-Objekte als Container an. Über new Object() lässt sich der Object-Konstruktor aufrufen und ein Object-Objekt erzeugen:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = new Object();
Container.eigenschaft = "wert";
Container.methode = function () {
	alert(this.eigenschaft);
};
Container.methode();

Der Name Container ist hier selbstverständlich als Platzhalter gemeint. Sie sollten das Object-Objekt (im Folgenden kurz Object genannt) sinnvoll nach der Aufgabe bzw. dem Zweck ihres Scriptes benennen.

Über die gewohnte Schreibweise zum Ansprechen von Unterobjekten werden dem Object weitere Objekte angehängt. Im Beispiel werden dem Object zwei Objekte angehängt, ein String und eine Funktion. Die entstehende Verschachtelung könnte man so illustrieren:

Da die Funktion methode ein Unterobjekt von Container ist, bezeichnet man sie als Methode dieses Objektes. Andere Unterobjekte, die nicht Funktionen sind, bezeichnet man als Eigenschaften.

Durch diese Beziehung bezieht sich das Schlüsselwort this innerhalb der Methode auf das Objekt, dem die Methode anhängt, im Beispiel Container. Ein Zugriff auf die Eigenschaft namens eigenschaft ist daher über this.eigenschaft möglich.

Auf dieselbe Weise können sich Methoden untereinander ansprechen und aufrufen. Zum Beispiel ließe sich dem Object eine zweite Methode hinzufügen, die die erste aufruft:

Container.zweiteMethode = function () {
	this.methode();
};
Container.zweiteMethode();

Wie ebenfalls aus den Beispielen ersichtlich wird, ist der Zugriff auf die Unterobjekte (Member) des Objects »von außen« nur über den Namen des Objects nach dem Schema Objectname.Membername möglich.

nach obennach unten

Object-Literale

JavaScript bietet für das Definieren von Objects 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:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = {
	eigenschaft : "wert",
	methode : function () {
		alert(this.eigenschaft);
	}
};
Container.methode();

Mittlerweile bedienen sich unzählige »unaufdringliche« Scripte dieser Schreibweise und sie hat sich zu einem Standard gemausert. Insbesondere Christian Heilmann hat sich für diese Schreibweise stark gemacht (deutschsprachige Seite Object Literal – Warum neuere Skripte anders aussehen), seine englischsprachige Seite Scripte sind gute Beispiele dafür, wie Object-Literale in der Praxis verwendet werden.

nach obennach unten

Object-Methoden in anderen Kontexten ausführen

Beim »unaufdringlichen« JavaScript ist es meist unerlässlich, dass im Object gespeicherte Methoden als Event-Handler dienen (siehe bereichsübergreifendes Kapitel Ereignisüberwachung mit JavaScript programmieren). Dies wirft das Problem auf, dass solche Methoden außerhalb des Object-Kontextes ausgeführt werden, wenn das überwachte Ereignis eintritt.

Außerhalb des Kontextes bedeutet, dass this nicht mehr wie beschrieben auf das Object zeigt, sondern auf das Elementobjekt, dessen Handler ausgelöst wurde. (Siehe englischsprachige Seite die Bedeutung des this-Schlüsselwortes beim Event-Handling.) In vielen Fällen aber ist im Event-Handler ein Zugriff auf beide Objekte gewünscht, das Elementobjekt sowie das Object.

Folgendes Beispiel illustriert das Problem:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = {
	eigenschaft : "wert",
	methode : function () {
		// Funktioniert:
		alert(this.eigenschaft);
	},
	handler : function (eventobjekt) {
		if (!eventobjekt)
			eventobjekt = window.event;
		// Fehler: this verweist auf das Element, dem der Event-Handler anhängt
		alert(this.eigenschaft);
	}
};
Container.methode();
document.getElementById("button").onclick = Container.handler;

Die Methode handler wird als Handler für das click-Ereignis bei einem Button definiert. Während der Zugriff auf das Object über this beim regulären Aufruf der Methode funktioniert, verweist this in diesem Fall auf das document-Objekt.

Dasselbe Problem tritt auf, wenn eine Methode eine andere Methode desselben Objects mit einer Verzögerung (setTimeout) oder als Intervall (setInterval) aufrufen will. this zeigt dann auf window, da die verzögert aufgerufene Methode im globalen Kontext aufgerufen wird:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = {
	eigenschaft : "wert",
	methode : function () {
		// Funktioniert:
		alert(this.eigenschaft);
		window.setTimeout(this.verzögert, 500);
	},
	verzögert : function () {
		// Fehler: this verweist window
		alert(this.eigenschaft);
	}
};
Container.methode();

Lösung: this vermeiden

Eine mögliche Lösung ist, das Object immer explizit über dessen Namen anzusprechen anstatt über this.

this wird dann nur noch in Methoden verwendet, die als Event-Handler dienen. Denn this ist die einzige Möglichkeit, im Internet Explorer auf das Element zuzugreifen, dessen Handler das Ereignis ausgelöst hat. In Browsern, die dem DOM-Events-Standard folgen, gibt es dafür die Eigenschaft currentTarget des Event-Objektes.

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = {
	eigenschaft : "wert",
	methode : function () {
		alert(Container.eigenschaft);
	},
	handler : function (eventobjekt) {
		if (!eventobjekt)
			eventobjekt = window.event;
		alert("Event-Objekt: " + eventobjekt);
		alert("Element, das den Event behandelt: " + this);
		alert("Container.eigenschaft: " + Container.eigenschaft);
	}
};
Container.methode();
document.getElementById("button").onclick = Container.handler;

In diesem Beispiel wurde this durch Container ersetzt. this wird in der Methode handler verwendet, um auf das Elementobjekt zuzugreifen, bei dessen Handler vom Ereignis ausgelöst wurde.

Das folgende Beispiel zeigt, wie this bei der Benutzung von setTimeout vermieden werden kann:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = {
	eigenschaft : "wert",
	methode : function () {
		alert(Container.eigenschaft);
		window.setTimeout(Container.verzögert, 500);
	},
	verzögert : function () {
		alert(Container.eigenschaft);
	}
};
Container.methode();

Solange eine Methode nicht in anderen Kontexten ausgeführt wird, kann darin this verwendet werden, um das Object anzusprechen. Aus Gründen der Einheitlichkeit und Einfachheit wurde in den Beispielen immer Container verwendet.

nach obennach unten

Möglichkeiten der Datenspeicherung

Das definierte Object, das alle Variablen und Funktionen eines Scriptes kompakt speichert, muss dokumentweit eindeutig sein. Es kann keine weiteren gleichnamigen globalen Objekte geben. Das heißt, es ist nur eine Instanz des Objects möglich.

Bei »unaufdringlichem« JavaScript wird gewissen Elementen Interaktivität hinzugefügt. Beispielsweise kann allen Tabellen im Dokument mit der Klasse sortierbar automatisch eine Sortier-Funktionalität hinzugefügt werden. Wenn also mehrere Tabellen sortierbar sind, muss z.B. der jeweilige Sortierstatus irgendwo gespeichert werden. Dazu bieten sich verschiedene Möglichkeiten an:

nach obennach unten

Eigene Objekte

Anstatt alle Eigenschaften und Funktionen an ein Object anzuhängen, kann man ein eigenes Objekt erstellen.

Aus anderen Programmiersprachen kennt man das Definieren von eigenen Klassen. In JavaScript gibt es strenggenommen keine Klassen, sondern nur Konstruktor-Funktionen (kurz: Konstruktoren), die mit dem Schlüsselwort new aufgerufen werden. Dabei wird intern ein neues, leeres Object angelegt und der Konstruktor im Kontext dieses Objektes ausgeführt. Im Konstruktor können diesem Objekt dann Eigenschaften und Methoden dann über this hinzugefügt werden.

Auch wenn das so entstehende Objekt der Object-Struktur ähnelt, können auf diese Weise unzählige gleiche Abkömmlinge, sogenannte Instanzen erzeugt werden.

Popup-Seite Anzeigebeispiel: So sieht's aus

// Konstruktorfunktion
function Container () {
	// Zugriff auf das neue Objekt über this,
	// Hinzufügen der Eigenschaften und Methoden
	this.eigenschaft = "wert";
	this.methode = function () {
		// In den Methoden wird über this auf das Objekt zugegriffen
		alert(this.eigenschaft);
	};
}
// Erzeuge Instanzen
var instanz1 = new Container();
instanz1.methode();
var instanz2 = new Container();
instanz2.methode();
// usw.

Indem der Konstruktor bestimmte Parameter erhält, können Instanzen mit unterschiedlichen Eigenschaften erzeugt werden. Sie können aber auch im Laufe der Benutzung unterschiedliche Werte bekommen. Der Zugriff »von außen« auf sogenannte öffentliche Eigenschaften erfolgt über das bekannte Schema instanzname.membername.

Die Bezeichnung eigenes Objekt ist unglücklich und missverständlich, schließlich haben wir mit dem Object ein eigenes Objekt erzeugt. Andere Quellen verwenden den bekannten Begriff Klasse auch für JavaScript-Konstruktoren. Allerdings führt diese Bezeichnung nicht weniger in die Irre, da sich die objektorientierte Programmierung in JavaScript grundlegend von der klassenbasierter Sprachen unterscheidet.

nach obennach unten

Methoden eigener Objekte in anderen Kontexten ausführen

Will man nun eine Methode einer Instanz als Event-Handler nutzen oder sie verzögert aufrufen, tritt das bekannte Phänomen auf: Die Methode wird außerhalb des Kontextes der Instanz ausgeführt und this zeigt nicht mehr auf die Instanz. Folgendes Kombinationsbeispiel veranschaulicht das Problem:

Popup-Seite Anzeigebeispiel: So sieht's aus

function Container () {
	this.eigenschaft = "wert";
	this.methode = function () {
		// Funktioniert:
		alert(this.eigenschaft);
		window.setTimeout(this.verzögert, 500);
	};
	this.verzögert = function () {
		// Fehler: this verweist window
		alert(this.eigenschaft);
	};
	this.handler = function (eventobjekt) {
		if (!eventobjekt)
			eventobjekt = window.event;
		alert("Event-Objekt: " + eventobjekt);
		// Fehler: this verweist auf das Element, dem der Event-Handler anhängt
		alert(this.eigenschaft);
	};
	document.getElementById("button").onclick = this.handler;
}
var instanz = new Container();
instanz.methode();

Die Lösung dieses Problems ist kompliziert und führt uns auf eine weitere hochinteressante, aber auch schwer zu meisternde Eigenheit der JavaScript-Programmierung, die im Folgenden vorgestellt werden soll.

nach obennach unten

Einführung in Closures

Eine Closure ist allgemein gesagt eine Funktion, die in einer anderen Funktion notiert wird. Diese verschachtelte, innere Funktion hat Zugriff auf die Variablen des Geltungsbereiches (Scopes) der äußeren Funktion.

Durch diese »Vererbung« der Variablen kann man bestimmte Objekte in Funktionen verfügbar machen, die darin sonst nicht oder nur über Umwege verfügbar wären. Closures werden damit zu einem Allround-Werkzeug in der fortgeschrittenen JavaScript-Programmierung.

Ein Beispiel, um Closures im Allgemeinen zu erläutern (erst einmal ohne eigene Objekte):

Popup-Seite Anzeigebeispiel: So sieht's aus

function äußerefunktion () {
	// Definiere eine lokale Variable
	var variable = "wert";
	// Lege die Closure als lokale Variable an
	var closure = function () {
		// Obwohl diese Funktion einen eigenen Scope mit sich bringt,
		// ist die Variable aus dem umgebenden Scope hier verfügbar:
		alert(variable);
	};
	// Führe die eben definierte Closure aus
	closure();
}
äußerefunktion();

Normalerweise werden alle lokalen Variablen einer Funktion aus dem Speicher gelöscht, nachdem die Funktion beendet wurde. Eine Closure führt dazu, dass die Variablen der äußeren Funktion nach deren Ablauf nicht gelöscht werden, sondern im Speicher erhalten bleiben und in der inneren Funktion weiterhin über deren ursprüngliche Namen verfügbar sind. Die Variablen werden also eingeschlossen und konserviert – daher der Name »Closure«.

Auch lange nach dem Ablauf der äußeren Funktion hat die Closure immer noch Zugriff auf die Variablen – vorausgesetzt, die Closure wurde woanders gespeichert und kann dadurch zu einem anderen Zeitpunkt ausgeführt werden, denn als lokale Variable würde sie selbst verfallen. Das Registrieren als Event-Handler ist eine Speichermöglichkeit:

Popup-Seite Anzeigebeispiel: So sieht's aus

function äußerefunktion () {
	var variable = "wert";
	// Lege die Closure-Funktion als lokale Variable an
	var closure = function () {
		alert(variable);
	};
	// Speichere die Closure-Funktion als Event-Handler
	document.getElementById("button").onclick = closure;
}
äußerefunktion();

Bei einem Klick auf das Dokument wird die Closure als Event-Handler ausgeführt. äußerefunktion wird schon längst nicht mehr ausgeführt, aber variable wurde in die Closure eingeschlossen.

Anwendung von Closures

Wie helfen uns Closures nun weiter? Alle Methoden, die der Instanz innerhalb des Konstruktors zugewiesen werden, wirken als Closures. In ursprünglichen Beispiel sind das methode, handler und verzögert.

Im Konstruktor kann man daher eine lokale Variable als bloße Referenz auf this anlegen. Alle Methoden, die der Instanz im Konstruktor hinzugefügt werden, schließen diese Variable ein – sie ist in diesen Methoden verfügbar, auch wenn sie als Event-Handler oder mit Verzögerung in einem ganz anderen Kontext ausgeführt werden. (Solche, die im Konstruktor notiert werden, werden private Eigenschaften genannt.) Folgendes Beispiel demonstriert beide Fälle:

Popup-Seite Anzeigebeispiel: So sieht's aus

function Container () {
	var thisObject = this;
	this.eigenschaft = "wert";
	this.methode = function () {
		// methode wirkt als Closure und schließt thisObject ein
		alert("methode: " + thisObject.eigenschaft);
		window.setTimeout(thisObject.verzögert, 500);
	};
	this.verzögert = function () {
		// verzögert wirkt als Closure und schließt thisObject ein
		alert("verzögert: " + thisObject.eigenschaft);
	};
	this.handler = function (eventobjekt) {
		// handler wirkt als Closure und schließt thisObject ein
		if (!eventobjekt)
			eventobjekt = window.event;
		alert("handler");
		alert("Event-Objekt: " + eventobjekt);
		alert("Element, das den Event behandelt: " + this);
		alert("Instanz-Eigenschaft: " + thisObject.eigenschaft);
	};
	// Hier im Konstruktor sind this und thisObject noch identisch
	document.getElementById("button").onclick = this.handler;
}
var instanz = new Container();
instanz.methode();

nach obennach unten

Closures zur Kapselung bei Object und eigenen Objekten

Bei eigenen Objekten lässt sich festlegen, welche Unterobjekte »von außen« eingesehen und geändert werden können. In der Fachsprache wird zwischen öffentlichen und privaten Membern unterschieden. Nach außen sollte eine Instanz eine wohlüberlegte und gut dokumentierte Programmierschnittstelle anbieten, in der scriptintern verwendete Variablen und Funktionen nicht vorkommen.

Dieses wichtige Konzept der Kapselung in der objektorientierten Programmierung soll hier nur kurz angeschnitten werden. Einen vollständigeren Einstieg bieten die Quellen in den nach unten Literaturhinweisen.

Bei den bisher vorgestellten Objects sind alle Unterobjekte öffentlich. Über einen Umweg sind auch private Member bei Objects möglich. Das Mittel dazu sind wieder Closures. Das Konzept lautet folgendermaßen:

In der ausführlichen Schreibweise könnte die Umsetzung so aussehen:

Popup-Seite Anzeigebeispiel: So sieht's aus

function erzeugeObject () {
	var privateEigenschaft = "privat";

	function privateMethode () {
		window.alert("privateMethode");
		window.alert(privateEigenschaft);
		window.alert(Container.öffentlicheEigenschaft);
	}

	var object = {
		öffentlicheEigenschaft : "öffentlich",
		öffentlicheMethode1 : function () {
			// öffentlicheMethode1 wirkt als Closure und
			// schließt privateEigenschaft und privateMethode
			window.alert("öffentlicheMethode1");
			window.alert(Container.öffentlicheEigenschaft);
			window.alert(privateEigenschaft);
			privateMethode();
			Container.öffentlicheMethode2();
		},
		öffentlicheMethode2 : function () {
			// öffentlicheMethode2 wirkt als Closure und
			// schließt privateEigenschaft und privateMethode
			window.alert("öffentlicheMethode2");
		}
	};

	return object;
}
var Container = erzeugeObject();
Container.öffentlicheMethode1();
// Ergibt undefined:
window.alert(Container.privateMethode);

Das Object hat schließlich zwei öffentliche Methoden, die ihrerseits Lese- und Schreibzugriff auf die privaten Eigenschaften und Methoden haben. Von außen sind diese privaten Member aber nicht sichtbar.

Bei der obigen Schreibweise muss es eine globale Funktion erzeugeObject geben, die aufgerufen wird. Diese ist im Grunde unnötig, da eine anonyme (namenlose) Funktion notiert als sogenannter Funktionsausdruck (Function Expression) ausreicht. Wir haben Funktionsausdrücke schon die ganze Zeit benutzt, wenn wir var this.methode = function ( … ) { … }; notiert haben – sie bilden den Gegenpart zur gewohnten Schreibweise von Funktionen, der sogenannten Funktionsdeklaration (Function Declaration).

Zudem kann die Variable object in der Funktion eingespart werden, indem direkt hinter return das Object-Literal notiert wird. Die Kurzschreibweise lautet des obigen Beispiels lautet demnach:

Popup-Seite Anzeigebeispiel: So sieht's aus

var Container = (function () {
	// Definiere eine Funktion mit einem Funktionsausdruck,
	// drumherum Klammern

	var privateEigenschaft = "privat";
	function privateMethode () {
		window.alert("privateMethode");
		window.alert(privateEigenschaft);
		window.alert(Container.öffentlicheEigenschaft);
	}

	// Direkt das Object zurückgeben
	return {
		öffentlicheEigenschaft : "öffentlich",
		öffentlicheMethode1 : function () {
			window.alert("öffentlicheMethode1");
			window.alert(Container.öffentlicheEigenschaft);
			window.alert(privateEigenschaft);
			privateMethode();
			Container.öffentlicheMethode2();
		},
		öffentlicheMethode2 : function () {
			window.alert("öffentlicheMethode2");
		}
	};
})();
// Ende der eingeklammerten Function-Expression, dahinter
// sofort () zum Aufruf der soeben definierten Funktion
Container.öffentlicheMethode1();
// Ergibt undefined, weil von außen nicht sichtbar:
window.alert(Container.privateMethode);

Diese Schreibweise mag auf den ersten Blick unverständlich scheinen, deshalb noch einmal aufgedröselt:

nach obennach unten

Alternativlösungen zur Kontext-Problematik: bind() und bindAsEventListener()

Derzeit sprießen explosionsartig neue JavaScript-Programmiertechniken aus dem Boden. Das Perl-Motto There is more than one way to do it lässt sich mittlerweile auch auf JavaScript anwenden. Für die hier beispielhaft gelösten Probleme gibt es viele andere Lösungsmöglichkeiten, von einfach bis kompliziert, von unelegant bis elegant. Die beschriebenen Lösungen sind bewusst einfach gehalten, da sie sich an Einsteiger richten – in diesem Artikel sollen lediglich gewisse ausgewählte Strukturen vorgestellt sowie deren praktische Eigenheiten diskutiert werden. Andere, mächtigere Strukturen sowie allgemeine Objektorientierte Programmierung sind nicht der direkter Gegenstand des Artikels. Die verlinkten Quellen in den nach unten Literaturhinweisen beschreiben fortgeschrittene Herangehensweisen sowie grundlegende Einführungen.

Es soll allerdings auf eine verbreitete Technik hingewiesen werden, mit der sich Objektmethoden einfach in bestimmten Kontexten ausführen lassen: Das JavaScript-Framework englischsprachige Seite Prototype bietet dazu zwei Funktionen namens bind() und bindAsEventListener() an. Beide werden über prototypische Erweiterung dem allen Funktionsobjekten hinzugefügt – daraufhin hat eine beliebige Funktion namens funktion die Methoden funktion.bind() und funktion.bindAsEventListener(). Auf die vielfältigen Möglichkeiten der prototypischen Erweiterung, die einen der Grundpfeiler der Objektorientierten Programmierung in JavaScript darstellt, soll an dieser Stelle nicht näher eingegangen werden.

Diese Helfermethoden geben dynamisch erzeugte Funktionsobjekte zurück, die die eigentlichen Funktionen umhüllen. In diesen Wrapper-Funktionen werden die vordefinierten JavaScript-Funktionen englischsprachige Seite apply() und englischsprachige Seite call() verwendet, um die eigentlichen Funktionen im Kontext des angegebenen Objektes auszuführen. Die Wrapper-Funktion wirkt als Closures, wodurch ihr die benötigten Objekte zur Verfügung stehen. (Anmerkung: apply() und call() werden derzeit in SELFHTML 8.1.1 noch nicht dokumentiert.)

Die Funktionen bind() und bindAsEventListener() sehen auführlich und kommentiert so aus:

// Erweitere alle Funktionsobjekte um eine Methode »bind«
// über die prototype-Eigenschaft des Function-Konstruktors.
Function.prototype.bind = function () {

	// Speichere die gegenwärtige Funktion in »method«.
	var method = this;

	// Die Funktion nimmt eine beliebige Anzahl von Parametern entgegen,
	// auf die über den »arguments«-Pseudoarray zugegriffen wird.
	// Wandle »arguments« mit einer Helferfunktion in einen echten Array um.
	var args = $A(arguments);

	// Nehme den ersten Parameter als das Objekt, in dessen Kontext
	// die Funktion ausgeführt werden soll. Speichere die verbleibenden
	// Parameter in »args«.
	var object = args.shift();

	// Notiere eine Function-Expression, die als Closure wirkt
	var wrapper = function () {

		// Die Closure schließt »method«, »object« und »args« ein.
		// Rufe die Funktion im Kontext des Objektes »object« auf, reiche
		// die Parameter weiter und gebe den Rückgabewert zurück.
		return method.apply(object, args);

	};
	// Gib die Wrapper-Funktion zurück.
	return wrapper;

};

// Erweitere alle Funktionsobjekte um eine Methode »bindAsEventListener«
// über die prototype-Eigenschaft des Function-Konstruktors.
Function.prototype.bindAsEventListener = function (object) {
	// Die Funktion nimmt einen Parameter entgegen, der
	// das Objekt darstellt, in dessen Kontext die gewünschte Funktion
	// ausgeführt werden soll.

	// Speichere die gegenwärtige Funktion in »method«.
	var method = this;

	// Notiere eine Function-Expression, die als Closure wirkt
	var wrapper = function (event) {

		// Die Closure schließt »method« und »object« ein.
		// Rufe die Methode im Kontext des Objektes »object« auf und
		// reiche das Event-Objekt durch.
		method.call(object, event || window.event);
	};

	// Gib die Wrapper-Funktion zurück.
	return wrapper;
};

Die bind()-Methode findet Verwendung bei Timeouts und Intervallen, bindAsEventListener() bei Event-Handlern. Der folgenden Code zeigt, wie sich das Kombinationsbeispiel aus dem vorigen Abschnitt mithilfe von bind() und bindAsEventListener() umsetzen lässt. Im Quellcode des Anzeigebeispiels findet sich auch die Helferfunktion $A().

Popup-Seite Anzeigebeispiel: So sieht's aus

function Container () {
	this.eigenschaft = "wert";
	this.methode = function () {
		window.setTimeout(this.verzögert.bind(this), 500);
	};
	this.verzögert = function () {
		alert("verzögert: " + this.eigenschaft);
	};
	this.handler = function (eventobjekt) {
		alert("handler");
		alert("Event-Objekt: " + eventobjekt);
		alert("Element, das den Event behandelt: " + this.button);
		alert("Instanz-Eigenschaft: " + this.eigenschaft);
	};
	this.button = document.getElementById("button");
	this.button.onclick = this.handler.bindAsEventListener(this);
}
var instanz = new Container();
instanz.methode();

Hier mag zunächst die seltsame Schreibweise this.verzögert.bind(this) und this.handler.bindAsEventHandler(this) irritieren. Diese Aufrufe hüllen verzögert und handler in Closures, sie werden daraufhin im Kontext der Instanz ausgeführt.

Der Unterschied gegenüber der vorherigen, einfacheren nach oben Closures-Methode mag zunächst nicht groß scheinen. Allerdings bringen bind() und bindAsEventListener() eine etwas andere Arbeitsmethode mit sich und sind zugleich vielseitiger. Mittlerweile haben diese Methoden weite Verbreitung auch außerhalb des Prototype-Frameworks gefunden.

Da this auf die Instanz zeigt, ist der Zugriff auf das Element, dessen Handler gerade ausgeführt wird (currentTarget), im Gegensatz zu den früheren Beispielen nicht über this möglich. Stattdessen muss das Element beim Registrieren des Event-Handlers in einer Eigenschaft an der Instanz gespeichert werden, im Beispiel this.button. In der Handler-Funktion erfolgt der Zugriff dann gleichermaßen über this.button.

Dies kann bei bestimmten Scripten, bei denen unzählige Event-Handler an verschiedenen Elementen registriert werden, zum Nachteil werden, wenn eine Unterscheidung zwischen target (im Internet Explorer: srcElement) und currentTarget (im Internet Explorer normalerweise this) nötig ist. Ein Event löst beim Aufsteigen (Bubbling) die Handler von Eltern-Elementen aus – das Ursprungselement ist daher nicht immer identisch mit dem Element, bei dem ein entsprechender Handler angestoßen wurde. Ein einfacher Zugriff auf dieses currentTarget ist mit einer unmodifizierten bindAsEventHandler()-Funktion nicht browserübergreifend möglich.

nach obennach unten

Modularisierung und Namensräume

Mittlerweile sind ganze JavaScript-Bibliotheken und -Frameworks entstanden, bestehend aus verschiedenen Modulen und Unterscripten. Bei zunehmender Komplexität ist es nicht mehr praktikabel, dass eine Ansammlung von aufeinander aufbauenden Scripten aus einer losen Ansammlung von Objects oder Konstruktoren bestehen.

Man geht daher dazu über, verwandte und zusammenhängende Objects und Konstruktoren in weitere Object-Container einzuordnen und zu verschachteln. Auf diese Weise entstehen mehrdimensionale Objektstrukturen, oft Module oder Pakete genannt. Einzelne Methoden werden dann über eine Kette von verschachtelten Objekten angesprochen, zum Beispiel YAHOO.util.Dom.methode() bei der englischsprachige Seite Yahoo! User Interface Library, dojo.dom.methode() beim englischsprachige Seite Dojo oder MochiKit.DOM.methode() bei englischsprachige Seite MochiKit.

Wie man an diesen Beispielen sieht, werden die Scripte nicht nur nach Funktionalität, sondern auch nach Zugehörigkeit zur Bibliothek geordnet. Eine Bibliothek besteht damit aus einem riesigen Object, das viele Unterobjekte enthält. Diese Ordnung nach Herkunft wird Namensraum (Namespace) genannt, in den obigen Beispielen YAHOO, dojo und MochiKit.

Bei kleineren zusammenhängenden Scripten lohnt sich ein eigener Namensraum nicht, sobald aber eine größere modularisierte Bibliothek entwickelt wird, bringen Namensräume Ordnung in die Scripte und sorgen dafür, dass sie nicht mit anderen kollidieren können.

Praktisch werden Namensräume über ein Object gelöst, das zunächst mit einem leeren Literal erzeugt wird. Danach können dem Object Member hinzugefügt werden:

var Namensraum = {};
Namensraum.Container = {
	…
};
Namensraum.Konstruktor = function (…) {
	…
};
var instanz = Namensraum.Konstruktor(…);

nach obennach unten

Ausblick auf JavaScript-Frameworks

Wir haben einige grundlegende formale Aspekte der Programmierung von »unaufdringlichem« JavaScript betrachtet. Diese bilden ein zuverlässiges Fundament für eine umfangreiche JavaScript-Anwendung.

Außen vor gelassen haben wir Probleme und Aufgaben, die uns immer wieder in der Praxis des DOM Scripting begegnen. Dies sind vor allem:

Mittlerweile werden mehrere JavaScript-Frameworks entwickelt, um diese grundlegenden Aufgaben von DOM Scripting zu lösen. Ziel ist es, dass der JavaScript-Programmierer all diese Aufgaben nicht immer wieder von Hand lösen muss. Anstatt direkt mit dem DOM zu programmieren, führen diese Frameworks zahlreiche Objekte und Methoden als Abstraktionsschicht ein. Diese sind einfacher und intuitiver zu bedienen und nehmen dem Webautor einen Großteil der Arbeit ab.

Trotzdem werden Frameworks wie englischsprachige Seite jQuery, englischsprachige Seite Prototype sowie die bereits genannten Dojo, MochiKit und Yahoo UI kritisch betrachtet. Sie legen einen einheitlichen Abstraktionslayer über die Browsereigenheiten, verbergen die tatsächlichen internen Vorgänge und geben vor, jedem einen Einstieg in die schwierige Materie des DOM Scripting zu ermöglichen.

Dabei ist es in vielen Fällen unverzichtbar, die interne Arbeitsweise zu kennen. Hier gilt: Wer die Aufgaben schon einmal »zu Fuß« gelöst hat und die Lösungsansätze kennt, steht nicht im Regen, wenn die Abstraktion in der Praxis nicht mehr greifen sollte.

Die meisten Helferscripte, Bibliotheken und Frameworks bedienen sich den vorgestellten Methoden zur Organisation. Die Kenntnis dieser Methoden ist daher nicht nur für das Schreiben von eigenen Scripten hilfreich, sondern auch für die Benutzung und das Verständnis von fremden Scripten.

nach obennach unten

Literaturhinweise

Einführung in Unobtrusive JavaScript und DOM Scripting

Programmiertechniken für strukturierte und wartbare Scripte

Closures

Objektorientierte Programmierung, speziell Kapselung

Moderne JavaScript-Bibliotheken und -Frameworks

Teil von SELFHTML aktuell Teil von Artikel Teil von JavaScript

© 2007 bereichsübergreifende Seite Impressum, für diese Seite: E-Mail molily@selfhtml.org