| E-Mail: |
|---|
Bei Fragen zu diesem Beitrag bitte den Autor des Beitrags kontaktieren!
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
Unobtrusive JavaScript, »unaufdringliches« JavaScript, oder auch
DOM Scripting.
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.
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).
ObjectSinnvoller 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.
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-ObjekteIn 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:
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:
Container (Object)
eigenschaft (String)methode (Function)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.
Object-LiteraleJavaScript 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:
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 (
Object Literal – Warum neuere Skripte anders aussehen), seine
Scripte sind gute Beispiele dafür, wie Object-Literale in der Praxis verwendet werden.
Object-Methoden in anderen Kontexten ausführenBeim »unaufdringlichen« JavaScript ist es meist unerlässlich, dass im Object gespeicherte Methoden als Event-Handler dienen (siehe
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
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:
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:
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();
this vermeidenEine 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.
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:
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.
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:
Object könnte dazu einen Array von Objects enthalten, in denen jeweils die Daten für eine Tabelle gespeichert werden.Object, sondern im Dokument selbst in Form von Attributen bzw. Unterobjekten der jeweiligen Elementobjekte gespeichert werden. In JavaScript können nämlich jedem Objekt beliebig Unterobjekte angehängt werden. So gehen viele Scripte vor, in den meisten Fällen ist dies auch der beste Weg, um mit einem dokumentweiten Object auszukommen.Object-Struktur als Container eignet sich dafür nicht, denn sie kann nicht ohne Aufwand beliebig dupliziert werden. Für diesen Fall eignen sich Eigene Objekte, deren Grundlagen im folgenden Abschnitt diskutiert werden.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.
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.
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:
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.
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):
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:
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.
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:
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();
Object und eigenen ObjektenBei 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
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:
Object-Literal. Die Methoden des Objects haben Zugriff auf die Variablen der äußeren Funktion und schließen diese ein (Closures).In der ausführlichen Schreibweise könnte die Umsetzung so aussehen:
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:
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:
function (…) {…}( function (…) {…} )( function (…) {…} ) ( … )Object – wird wie üblich gespeichert: var object = ( function (…) {…} ) ( … );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
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
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
apply() und
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().
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
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.
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
Yahoo! User Interface Library, dojo.dom.methode() beim
Dojo oder MochiKit.DOM.methode() bei
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(…);
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:
XMLHttpRequest)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
jQuery,
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.
© 2007
Impressum, für diese Seite:
molily@selfhtml.org