Objektorientiertes Programmieren mit JavaScript?
9.09.2006 on 15:27 | In Computer & IT, Medien & Internet | 10 CommentsJavaScript ist in der Tat eine objektorientierte Programmiersprache. Einige nennen sie lieber objektbasiert, aus dem Grund, da sie das Klassenprinzip anderer objektorientierter Hochsprachen nicht kennt. In Java, C++ und Konsorten erstellt man eine Klasse mit Methoden und Members als Schablone für die späteren Objekte. Diese Möglichkeit hat man bei JavaScript auf den ersten Blick nicht.
In JavaScript kann man keine Klassen anlegen. Man kann zwar irgendwie Objekte erzeugen. Eben dieses irgendwie möchte ich hier kurz erläutern, da mir in den letzten Monaten aufgefallen ist, dass sich die meisten JavaScript-Einführungen, a lá SelfHTML o.ä., sich nur sehr oberflächlich damit befassen: Wie macht man eine einfache Eingabevalidierung und ein paar grafische Effekte. Das ganze aber immer prozedural und im globaeln Namespace. Dabei bietet JavaScript alles was eine Hochsprache zum objektorientoerten Programmieren mitbringen muß: Deklaration von öffentlichen und privaten Members und Vererbung.
Zunächst mal ein Stück klassischer Code wie er in einen Script-Tag einer HTML-Seite vorkommen könnte:
function getSquare(val) {
return val * val;
}
var inputs = new Array();
inputs = document.forms[0].elements;
for (var i = 0; i < inputs.length; i++) {
alert(getSquare(inputs[i]));
}
So ähnlich sieht klassischer JavaScript Code aus. Für einfache Spielereien langte das auch. Doch mit dem Web 2.0 Hype und den komplexeren JavaScript-Anwendungen wird es notwendig etwas mehr Struktur in seienn JavaScript-Code zu bringen.
Was ist das Problem an dem obigen Code? Die Funktion getSquare und die Variablen i und inputs sind global defineirt. Das war bisher nur ein kleines Problem. Wird aber zunehmend zu einem, wenn man Bibliotheken (z.B. Prototype, Script.aculo.us, Dojo etc.) Dritter einsetzt. Dann kann sich sehr schnell ein Namensraumkonflikt ergeben. Und zwar dann wenn in solch einer Bibliothek eine Funktion oder Variable den gleichen Namen hat. Bindet man in obigen Code eine Bibliothek ein die auch eine Funktion getSquare deklariert, dann würde zwar nicht zwangsläufig ein Fehler auftreten. Allerdings würde nur eine der beiden Methoden benutzt werden. Entweder die die zuerst deklariert wurde oder die die als letztes im gesamten Code steht würde die erste überschreiben, je nachdem wie der JavaScript-Interpreter des Browsers es handhabt. Dies würde zu sehr seltsamen Effekten führen. Wie man dies umgeht beschreibe ich weiter unten in dem
Unterpunkt Namespaces.
Alles ist ein Objekt
Nun stellen sich sicher einige die Frage, wie man denn objektorientiert mit JavaScript programmiert. Es gibt keine Klassen und die Schlüsselwörter public und private gibt es auch nicht. Nebenbei sei hier noch erwähnt, dass in JavaScript alles ein Objekt ist. Selbst Funktionen sind ein Objekt. Hinzu kommt dann noch die Verwirrung darüber, dass man ein Objekt in Javascript auf verschiedene Arten notieren kann:
Ein einfaches Objekt über den Konstruktor Object erzeugen:
meinObjekt = new Object();
meinObjekt.eineEigenschaft = "Blafasel";
meinObjekt.nochEineEigenschaft = 42;
Jetzt hab ich mir einfach eine Instanz des Objekts Object erzeugt. Dieses ist sozusagen die Mutter aller Objekte in JavaScript. Vergleichbar mit dem Object in Java, Objective-C oder Smalltalk. Ich habe zwei Eigenschaften hinzugefügt. Alles bis jetzt ganz einfach.
Alternativ kann man ein Objekt auch über die JSON-Notation erzeugen:
meinObjekt = {
eineEigenschaft: "Blafasel";
nochEineEigenschaft: 42;
}
Klassen und Instanzen
Jetzt hab ich zwar ein Objekt. Aber nur eines. Wenn man aber gerne mehrer Objekte haben möchte die gleich aufgebaut sind kommt man um Klassen nicht herum. Und tatsächlich gibt es die Möglichkeit Objekte als Schablonen zu erzeugen, um weitere Objekte mittels new-Operator zu erzeugen. Hierfür muß man lediglich einen Konstruktor bereitstellen, der dann mit dem new-Operator aufgerufen werden kann:
function meinObjekt(val) {
this.val = val;
// ...
}
Diese Notation ist sicher bekannt und einige werden jetzt sagen: "Moment mal, das ist doch einfach nur eine Funktion." ja, genau. Aber wie ich oben beschrieb sind Funktionen ja auch Objekte und Objekte können ja wiederum Objekte beinhalten. Des weiteren ist der function-Operator gleichzeitig das Schlüßelwor um auch einen Konstruktor einzuleiten. In obigem Besipiel habe ich also einen Konstruktor für ein Objekt meinObjekt deklariert, welcher die Membervariable des Objektes (this.val) initialisiert. Im nächsten Abschnitt werde ich beschreiben wie man public und private Modifier umsetzt.
public und private
Das deklarieren von öffentlichen und privaten Eigenschaften und Methoden ist, wie bereits erwähnt, möglich, aber nicht wie gewohnt über die üblichen Modifier public oder private. Zunächst etwas Code:
function meinObjekt (val1, val2) {
var val1 = val1;
this.val2 = val2;
var _privateMethode = function () {
window.alert(val1);
};
this.privilegiertePublicMethode = function () {
window.alert(val1 " " + this.val2);
_privateMethod();
};
};
meinObjekt.prototype.unpriviligiertePublicMethode = function () {
window.alert(this.val2);
};
Was sofort ins Auge springt: Es werden hier objekte im Konstruktor (der Funktion meinOnjekt) angelegt. Allerdings mal mit var und mal mit this. Der Trick ist nun, dass Objekte (Wir erinenrn uns: Alles in JavaScript ist ein Objekt, egal ob Membervariable oder Methode.) die mit var deklariert werden nur im umgebenden Block (durch geschweifte Klammern begrenzt) sichtbar sind. Die Membervariable val1 ist also nur innerhalb des Konstruktors sichtbar und somit private. Im Gegensatz dazu sind alle Objekte die an das Objekt (this ist das Objekt, die
Konstruktorfunktion selbst) "angehängt" werden auch ausserhalb des Konstruktors sichtbar. Also public. Dies gilt für Variablen wie Methoden. Da ja alles, ich wiederhole mich, ein Objekt ist. Auch die "nativen" Datentypen für Variablen.
Eine kleine Feinheit gibt es allerdings bei Methoden zu berücksichtigen. Private Methoden werden einfach mit var deklariert, genau so wie private Membervariablen. Um öffentliche Methoden zu
deklarieren gibt es zwei Möglichkeiten:
- Die Methode wird in der Konstruktorfunktion mit
thisdeklariert. Diese Methode wird als privilegiert bezeichnet, da sie auf alle Members des Objektes zugreifen kann, auch die privat deklarierten. - Mit dem Schlüsselwort
prototypekann an ein bereits deklariertes Objekt (auch zur Laufzeit) eine weitere Eigenschaft hinzugefügt werden. Darauf gehe ich bei der Vererbung näher ein. Eine sog. unpreviligierte Methode wird wie im obigen Beispiel mitprototypeder Konstruktorfunktion hinzugefügt. Diese hat allerdings nur auf die öffentlichen Methoden und Variablen des Objekts Zugriff. Daher die Bezeichnung unpriviligiert.
Um von der obigen "Klasse"" nun ein Objekt zu erzeugen genügt folgender Code:
var meinErstesObjekt = new meinObjekt("privater Wert", "public Wet");
Vererbung
Jetzt stellt sich nur noch die Frage wie man Vererbung hin bekommt, dann steht einem alles für objektorientiertes Programmieren zur Verfügung. Die Antwort hierauf ist: prototype. In JavaScript verfügt jedes Objekt über die Eigenschaft prototype. Alles was dieser Eigenschaft zugewiesen wird, wird dem Objekt hinzugefügt. Dies wird manchmal protoypbasierte Vererbung genannt. Was sich hier so komplex anhört sieht im Code ganz simpel aus:
function Basisklasse(val) {
var val = val;
this.getVal = function() {
return val;
};
};
function Abgeleiteteklasse(val) {
this.constructor(val);
this.square() {
return getVal() * getVal();
};
};
Abgeleiteteklasse.prototype = new Basisklasse();
Ich deklariere zunächst einmal zwei "Klassen" (Basisklassse und Abgeleiteteklasse) in dem ich zwei konstruktorfunktionen schreibe. Nun kann ich die Abgeleiteteklasse ganz einfach von Basisklasse erben lassen in dem ich der prototype-Eigenschaft von Abgeleiteteklasse ein Objekt vom Typ Basisklasse zuweise. Dieses Objekt erzeuge ich wieder mit dem new-Operator. Somit werden alle Eigenschaften (Membervariablen und Methode) in AbgeleiteteKlasse kopiert. Über Abgeleiteteklasse kann man nun auf allle öffentlichen Eigenschhaften zugreifen. Auf private nicht. folgendes ist also möglich:
var abgeleitet = new Abgeleiteteklasse(5);
alert(abgeleitet.getVal());
Folgendes hingegen ist nicht möglich:
var abgeleitet = new Abgeleiteteklasse(5);
alert(abgeleitet.val);
Was noch zu sehen ist: Mittels this.constructor kann die Konstruktorfunktion der Basisklase aufgerufen werden. Dies ist im obigen Fall nötig um die private Variable val zu initialisieren, da auf die Variable in der abgeleiteten Klasse kein direkter Zugriff besteht.
Namespaces
Wie oben bereits angesprochen bekommt man Probleme wenn Funktionen oder Variablen, die mit dem gleichen Bezeichner deklariert wurden, auftauchen. Gleiches gilt für mehrere Konstruktorfunktionen mit dem gleichen Namen. Hier greift nun das Konzept von Namespaces oder Namensräume. Was bedeutet dies? In Java werden Namensräume über die sog. Packages abgebildet. Eine Klasse gehört immer zu einem Paket und ein Paket gehört zu einer URL:
de.svenspace.meinPaket.MeineKlasse
Alles unterhalb der URL wird dann in Verzeichnissen die den Namen des Paketes haben organisiert. Somit sind Namenskonflikte ausgeschlossen.
Ein solch bequemes System steht in JavaScript leider nicht zur Verfügung. Hier muß man sich etwas trickreicher behelfen:
var MeineBibliothek = function() {
this.Entwickler = "Sven.Space";
this.Version = "1.0";
//...
};
MeineBibliothek.ErsteKlasse = function(val) {
var val = val;
this.getVal = function() {
return val;
};
};
MeineBibliothek.ZweiteKlasse(val) {
this.constructor(val);
this.getSquare = fuunction() {
return getVal() * getVal();
};
};
MeineBibliothek.ZweiteKlasse.prototype = new MeineBibliothke.ErsteKlasse();
Was wird im obigen Code gemacht? Zunächst lege ich ein globales Objekt MeineBibliothek an. In diesem definiere ich einige Konstanten mit Informationen über die Bibliothek. Dies ist nicht zwingend notwendig ist aber ein Zeichen guten Stils. Dieses globale Objekt dient mir nun als Namespace, indem ich alle meine "Klassen" an dieses Objekt hänge. Meine Konstruktorklasse ist also eine Eigenschaft des Namespace-Objektes. Somit habe ich Namenskonflikte ausgeschlossen. Es sei denn es definert jemand ein gleiches globales Namespace-Objekt. Daher sollte man möglichst eindeutige Namen für sein Namespace-Objekt wählen. Ansonsten verfährt man mit allem weiteren gleich. Ausser dass man nun die Konstruktormethoden anders ansprechen muß:
var o = new MeineBibliothek.ZweiteKlasse(5);
alert(o.getSquare());
Eine kleiner Unterschied besteht lediglich in der Notation der Konstruktorfunktion. In allen vorangegangenen Beispielen wurden die Kosntruktorfunktion mit dem Schlüßelwort function eingeleitet und dahinter der Bezeiichner und die Paramaterliste. In der Namespacevariante wird zuerst der volle Bezeichenr (mit dem Namespace) notiert und dieser (neu hinzugefügten) Eigenschaft eine Funktion zugewiesen. Zu beachten ist dass man den Zuweisungsoperator (=) benutzen muß um der neuen "Konstruktoreigenschaft" des Namespaceobjektes die Kosntruktorfunktion zuzuweisen. Zudem wird die Paramaterliste hinter dem Schlüsselwort function notiert.
Natürlich sind beliebig viele Hierarchieebenen so möglich:
var o = new MeineBibliothek.Paket1.Unterpaket1.MeineKlasse();
Um dies zu realisieren fügt man einfach dem globalen Namespaceobjekt weitere Objekte hinzu die wiederum als Namespacecontainer fungieren:
var MeineBibliothek = {
// ...
};
MeineBibliothek.Paket1 = {
// ...
};
MeineBibliothek.Paket1.Unterpaket1 = {
// ...
};
MeineBibliothek.Paket1.Unterpaket1.MeineKlasse = function() {
// ...
};
this and that
Zum Schluß möchte ich noch auf eine besonderheit des Wörtchens this eingehen.
In objektorientierten Programmiersprachen referenziert this immer das umgebende Objekt in dessen Kontext das this notiert ist. In den obigen beispielen habe ich
das unter anderem dazu benutzt um einer Konstruktorfunktion (die auch ein Objekt ist) weitere eigenschaften (Variablen und Methoden ) hinzuzufügen. In solch einer Methode kannn man nun mittels this auf das Objekt zugreifen zu welchem diese Methode gehört. Inkonsequenterweise zeigt dieses this nicht auf das Funktionsobjekt das die Methode ja ist, was eigentlich zu erwarten wäre. Dieses Verhalten wird bei folgender Konstelation probelmatisch:
function Objekt1(val) {
this.val = val;
this.getSquare() {
return this.val * this.val;
};
};
function Objekt2() {
this.printMessage = function(val) {
alert(val());
};
};
var o1 = new Objekt1(5);
var o2 = new Objekt2();
o2.printMessage(o1.getSquare);
Da in JavaScript -- ich weiß dass ich mich wiederhole -- alles ein Objekt ist, kann man natürlich auch Funktionen und Methoden als Parameter an eine andere Funktion weitergeben und diese dort ausführen. Dies macht die Funktion printMessage(val). Ich übergebe ihr mit o1.getSquare (man beachte die fehlenden Klammern) die Methode als funktionsobjekt. Diese Methode wird dann in printMessage(val) ausgeführt in dem der Eigenschaft Klammern angfefügt werden: alert(val());. Allerdings ändert sich hierdurch der Ausführungskontext der übergebenen Methode. Dies hat zur folge, dass das this in getSquare() nicht mehr auf o1 sondern auf o2 zeigt. Der JavaScript-Interpreter wird mit der Fehlermeldung, dass Objekt2() nicht über eine Eigenschaft val verfügt abrechen.
Umgehen kannn man dieses "merkwürdige" Verhalten in dem man in der Konstruktorfunktion eine private Membervariable deklariert, welche die this-Referenz speichert und nur noch diese Memebervariable anstelle von this benutzt:
function Objekt1(val) {
var self = this;
this.val = val;
this.getSquare() {
return self.val * self.val;
};
};

Die Erde ist eine Scheibe
Kommentar by Mister_Maik — 12. Sep. 2006 #
Das denken so einige
Kommentar by 5V3N.5P4C3 — 12. Sep. 2006 #
Hey, das reimt sich…!!!
Unser erster gemeinsamer Reim…
“Jo, die Erde ist eine Scheibe – das denken so einige – yeah…”
Kommentar by Mister_Maik — 12. Sep. 2006 #
Java Script ist mir zwar so sympathisch wie Magen Darm Grippe, aber Thumbs UP. Aber der Beitrag ist richtig gut geschrieben.
Kommentar by solarix — 13. Sep. 2006 #
Es ist gewöhnungsbedürftig. Zum einen gibts noch nicht wirklich gute IDEs für JavaScript und zum anderen ist die Syntax halt recht ungewohnt. Was auch ätzend ist: Crossbrowserkompatibilität. Allerdings find ichs auch nicht schlimmer als C++ *lach*
Kommentar by 5V3N.5P4C3 — 13. Sep. 2006 #
Ich bin gottfroh das ich keine Browserkompatibilität mehr beachten muss, das hat mir vor vier Jahren gereicht, dieser Rotz mit Netzkasper und IE. Gott sei dank, durfte ich das mittlerweile alles verdrängen.
Kommentar by solarix — 13. Sep. 2006 #
endlich mal mein passwort zurückgesetzt…
also netter artikel, wollte nur noch mal an die
bösen memory leaks des IE erinnern.
Mehr dazu hier: Link
Kommentar by spider — 18. Sep. 2006 #
Jaja, und immer diese Probleme mit validem HTML *g* Ich hab deine nLink mal angepasst. Guter Artikel. Der IE nervt mich eh ziemlich. Hast du nen Artikel über Events? Bei mir tun manche Events nicht… *grml*
Andererseits hab ich aber auch das Gefühl, dass der Firefox auch unter Leaks leidet. Oder unter was auch immer. Das dingen friert mir permanent ein und überlegt ne halbe Minute was er tun soll.
Um ein bisschen den Leaks entgegen zu wirken hab ich mir angewöhnt dem body-tag-event
onunloadnedeinit()zuzuweisen, die alle events deregistriert und alle von mir erstellten Objekte deleted.Ich glaub ich hab zu lang C++ programmiert *lol*
Kommentar by 5V3N.5P4C3 — 18. Sep. 2006 #
Gratuliere! Endlich mal jemand, der das ganze auf Deutsch zusammenfasst. Deine Meinung zu JavaScript hat sich ja auch geänder, hihi.
Einige der Konstrukte finden sich auch in Log4js wieder: http://log4js.berlios.de
Da habe ich auch erst “OO” in JavaScript gelernt.
Kommentar by stritti — 18. Sep. 2006 #
Du Schleichwerber *lol* Log4JS muß ich mir aber mal ganz genau anschauen was ma nda refactorn kann, denn das Dingen ist so wahnsinnig lahmarschig
Kommentar by 5V3N.5P4C3 — 18. Sep. 2006 #