Gralssuche
Objektorientierte Konzepte haben vor Jahren ihren Weg in JavaScript gefunden. Allerdings hĂĽllen sie sich hier in ein anderes Gewand.
- Dr. Cai Ziegler
Die Zeiten, da OOP als Hirngespinst theoretisierender Universitätsprofessoren abgetan wurde, sind passé. In praktisch allen Programmiersprachen sind die Konzepte der objektorientierten Programmierung verwirklicht, und deren Anwendung ist seit geraumer Zeit gängige Praxis in der professionellen Entwicklung.
Vor allem mit der rasanten Entwicklung des Webs bekamen Skriptsprachen einen höheren Stellenwert; Programmiersprachen, deren Code nicht kompiliert, sondern von einem Interpreter ausgeführt wird. Zielgruppe dieser Skriptsprachen waren nicht nur Software-Entwickler. Auch Grafiker, Systemadministratoren und interessierte Heimwerkernaturen sollten einfache Programme auf diese Weise schreiben können. Von professionellen Entwicklern zunächst belächelt, haben sich Skriptsprachen mittlerweile fest etabliert, und ihre Anwendung erstreckt sich auf nahezu alle Bereiche der Informationstechnik. Mit der Verbreitung wuchsen die Anforderungen, da jene heute sogar zur Realisierung größerer Projekte herangezogen werden. Als problematisch erwies sich hier jedoch das Fehlen grundlegender Konzepte wie Typisierung und Modularität. Auch die Objektorientierung, Voraussetzung für einen sauberen Entwurf gemäß den Erkenntnissen des Software-Engineering, war Skriptsprachen zunächst fremd.
JavaScript ist wohl der bekannteste Vertreter jener neuen Gattung von Sprachen und wird vor allem im Web sowohl auf Seiten des Clients als auch des Servers eingesetzt. Dass JavaScript mittlerweile als objektorientiert bezeichnet werden kann, ist vielen bis dato verborgen geblieben. Allerdings ist die Realisierung der Objektorientierung in JavaScript sehr verschieden davon, wie das Gros der herkömmlichen Programmiersprachen sie umsetzt.
Klassen und Prototypen
Die Grundfesten der Objektorientierung in Sprachen wie C++ oder Java bilden die Konzepte der Klasse und des Objekts. Bei diesem Ansatz sind die beiden Begriffe nicht deckungsgleich und dürfen unter keinen Umständen vertauscht werden: Eine Klasse ist ein strukturierter beziehungsweise komplexer Typ, der als eine Art Vorlage für die zugehörigen Objekte, auch Instanzen genannt, dient. Letztere werden nach dem vorgegebenen Schema der Klasse erzeugt. Weiterhin definieren Klassen lediglich die Datenstruktur und die entsprechenden Methoden, auf ihnen selbst kann jedoch nicht operiert werden. Das ist nur mit den konkreten Ausprägungen, eben jenen Objekten, möglich. Andererseits kann kein Objekt ohne eine zugeordnete Klasse existieren und eine Klasse ohne daraus gestanzte Instanzen ist wertlos für die praktische Arbeit. Die Sichtweise ist hier eher mengenorientiert, wobei die Klasse als Definition der Menge und die Objekte als die darin enthaltenen Tupel betrachtet werden können.
In JavaScript implementiert ist hingegen das so genannte Konzept der Prototypen. Dieser Ansatz differenziert nicht zwischen Objekten und Klassen, sondern kennt nur das Konzept der Objekte. Natürlich muss auch hier eine Vorlage für das Erzeugen von Objekten dienen. Im Gegensatz zum klassenbasierten Ansatz ist dies aber nicht eine Klassenschablone, sondern ein so genanntes prototypisches Objekt. Da diese Vorlage jedoch selbst ein Objekt ist, können beliebige Operationen auf ihm ausgeführt werden, was mit Klassen nicht möglich ist, sieht man einmal vom Konzept der statischen Klassen unter Java ab.
Allerdings impliziert das Fehlen der konkreten Richtlinien, die eine Klasse vorgibt, dass Objekte generell an keine Maßgaben gebunden sind, da alle für sich stehen und die prototypischen Objekte nur Initialwerte vorgeben: Jedes Objekt kann zur Laufzeit um beliebige Methoden und Attribute erweitert werden. Eine strikte Typisierung der Sprache ist deshalb praktisch unmöglich, denn schließlich existieren keine Typen oder Klassen, die Restriktionen der Instanzen formulieren.
Zunächst scheint es, dass diese Konzeption im Hinblick auf die Vererbung und den Aufbau einer Generalisierungshierarchie enorme Probleme aufwirft, und tatsächlich erfordert die Sichtweise eine Umgewöhnung, wenn man den Umgang mit der klassenbasierten Objektorientierung gewohnt ist. Andererseits ist auch Smalltalk, die Mutter aller objektorientierten Sprachen, untypisiert.
Im Folgenden werden die Unterschiede der beiden Konzeptionen durch Beispiele verdeutlicht, doch bleibt festzuhalten, dass der Ansatz von JavaScript nicht unbedingt schlechter ist; er stellt lediglich eine andere und zudem mächtige Alternative zur klassenbasierten Sichtweise dar. Zur Entwicklung der prototypenbasierten Objektorientierung sei noch erwähnt, dass diese durch Self begründet wurde, eine Sprache, die Forscher der Stanford University vor fast zehn Jahren kreierten (siehe [2]).
Listing 1: Shape
function Shape (centerX, centerY, color) {
// Eigenschaften
this.centerX = centerX;
this.centerY = centerY;
this.color = color;
// Methoden
this.move = moveShape;
}
Nach diesen theoretischen Erwägungen soll es nun um Objekte unter JavaScript gehen. Anstatt einer Klassendefinition gibt es hier nur eine Konstruktormethode, die ein Objekt mit Anfangswerten erstellt. Sie ist somit ein Prototyp und kann als Schablone für weitere Objekte dienen. Listing 1 legt Maßgaben für einen Prototypen fest, der drei Attribute und eine Methode enthält. Die Eigenschaften werden mit den Übergabeparametern des Konstruktors initialisiert, die Methode moveShape () muss an einer beliebigen Stelle im Skript definiert sein, da sonst eine Fehlermeldung des Interpreters erscheint. Das vorangestellte Schlüsselwort this gibt an, dass es sich um Eigenschaften beziehungsweise Methoden des Objekts handelt. Soll ein neues Objekt gemäß dem Konstruktor gestanzt werden, kann dies zum Beispiel folgende Zeile bewirken:
var oShape = new Shape (15, 25, "#ff3300");
Dies ist praktisch identisch zum Erzeugen von Objekten unter Java, und auch der Zugriff auf Attribute und Methoden geschieht in beiden Fällen über den Punktoperator. Betrachtet man den Konstruktor, scheint es zunächst, als verbärge sich hinter diesem doch das Klassenkonzept. Immerhin gibt er ein Schema für sämtliche Instanzen vor. Allerdings ist diese Vorgabe nicht restriktiv, sondern definiert nur einen Initialzustand für Objekte, die nicht an jenes Schema gebunden sind, sondern um Attribute oder Methoden erweitert werden können, was die klassenbasierte Sicht nicht zuließe. Will man beispielsweise dem soeben erzeugen Objekt noch die Eigenschaft cntVertices und die Methode scale() hinzufügen, erledigen dies folgende zwei Zeilen:
oShape.cntVertices = 8;
oShape.scale = scale;
Diese Modifikation bezieht sich jedoch ausschließlich auf das Objekt oShape, sämtliche anderen vom Konstruktor Shape() erzeugten Instanzen bleiben hiervon unberührt.
Vererbung und die Prototypenkette
Soll hingegen eine Eigenschaft oder Methode sämtlichen von einem speziellen Konstruktor erzeugten Objekten hinzugefügt werden, erfordert das die Erweiterung des Prototyps. Dies ermöglicht die im Sprachumfang von JavaScript vordefinierte Eigenschaft prototype:
Shape.prototype.cntVertices = 8;
Nach diesem Statement enthalten alle Objekte, die auf den Prototyp Shape zurückzuführen sind, die Eigenschaft cntVertices, die auf den Initialwert 8 gesetzt ist. Die Eigenschaft prototype ist auch der Schlüssel zur Realisierung von Vererbung unter JavaScript. Zur Erläuterung der Vererbung dient die Terminologie der klassenbasierten Objektorientierung, denn die Begriffe Superklasse und Subklasse existieren eigentlich im Umgang mit Prototypen nicht.
Sei nun der Prototyp Shape die Superklasse, die spezialisiert werden soll. Die Subklasse heißt Circle und erweitert Shape lediglich um die Eigenschaft diameter (Durchmesser des Kreises). In Java sähe eine derartige Spezialisierung etwa aus wie im oberen Teil von Listing 2.
Listing 2: Vererbung
/* Vererbung in Java */
public class Circle extends Shape {
public int diameter;
public Circle (centerX, centerY, color, diameter) {
super (centerX, centerY, color);
this.diameter = diameter;
}
}
/* Vererbung in JavaScript */
function Circle (centerX, centerY, color, diameter) {
this.diameter = diameter;
this.centerX = centerX;
this.centerY = centerY;
}
Circle.prototype = new Shape ();
Hierbei ersetzt der spezielle den generischen Konstruktor, fordert fĂĽr alle Eigenschaften Initialwerte und nutzt sie, um den Konstruktor der Superklasse aufzurufen. Die analoge Notation in JavaScript befindet sich im unteren Teil des Listings.
Der Definition des Konstruktors folgt eine Zeile, die der Eigenschaft prototype ein neues Objekt des Prototyps Shape übergibt. Um eine solche Instanz werden folglich alle Objekte von Circle erweitert. Mit anderen Worten, sie erben die Attribute und Methoden von Shape. Besondere Aufmerksamkeit verdienen die beiden Zeilen nach der Spezifizierung der Eigenschaft diameter: Hier werden die beiden Attribute centerX und centerY vom geerbten Shape überschrieben. Der Grund hierfür ist, dass es unter JavaScript nicht möglich ist, beim Erstellen einer Hierarchie den Konstruktor der Superklasse aufzurufen.
Das Prototypenkonzept, das Hinzufügen von Eigenschaften und Methoden zur Laufzeit und das Erzeugen der Hierarchie erscheinen zunächst undurchsichtig. Ein Blick auf die Interna der Realisierung macht diese Konzepte nachvollziehbar:
var oCircle = new Circle (12, 13, "#ff0000", 5);
Beim Anlegen des Objekts oCircle ruft der Interpreter über den Operator new den Konstruktor Circle auf und gibt das resultierende Objekt als Referenz zurück. Weiterhin erhält die interne Eigenschaft __proto__ von oCircle den Wert von Circle.property. Im Übrigen sei erwähnt, dass der Name dieser internen Eigenschaft nur unter Netscape __proto__ lautet; unter dem Internet Explorer existiert hier zwar eine semantisch äquivalente Variable, doch ist kein Zugriff der Kernsprache auf diese erlaubt und so jene auch in keiner Referenz explizit aufgeführt.
Vom Prototyp geerbt
Das Objekt oCircle besitzt keine lokalen Werte, sondern nur die vom Prototyp Circle geerbten. Weiterhin enthält oCircle, da es eine Subklasse von Shape ist, einen Verweis auf ein Objekt des Prototyps Shape. Auch dieses besitzt die Eigenschaft __proto__, die auf Shape zeigt und deshalb dessen initialisierte Attribute erbt. Man spricht hier von einer Prototypenkette, was in etwa gleichbedeutend mit der Generalisierungshierarchie in klassenbasierten Sprachen ist. Da es sich um eine Kette handelt, ist klar, dass das Konzept der Prototypen keine Mehrfachvererbung unterstützt: Jedes Objekt besitzt nur eine Eigenschaft __proto__, die Hierarchie ist somit eine Generalisierungslinie.
Beim Zugriff auf Eigenschaften eines Objekts überprüft der Interpreter, ob eine lokale (= nicht geerbte) Eigenschaft mit dem gesuchten Namen existiert. Ist dies nicht der Fall, durchläuft er die Prototypenkette absteigend auf der Suche nach dem Attribut. Wird es auf dem Pfad durch die Hierarchie nicht gefunden, erfolgt die Rückgabe des Werts undefined als Signal, dass das Objekt das fragliche Attribut nicht besitzt. Beim Zugriff auf die Eigenschaft centerX von oCircle wird zunächst nach einem lokalen Vorhandensein gesucht, dies schlägt fehl und somit wird der Inhalt von __proto__ ausgelesen. Da Circle.prototype.centerX definiert ist, endet die Suche an dieser Stelle; andernfalls würde der Prototyp Shape noch untersucht. Stünde jedoch beispielsweise die folgende Zeile nach dem Instanziieren von oCircle, würde bereits die lokale Suche erfolgreich terminieren:
oCircle.centerX = 42;
Zusammenfassend lässt sich festhalten, dass die Angabe des Konstruktors ein Objekt definiert: den Prototyp. Da lediglich eigenständige Objekte, die nicht an Klassen gebunden sind, existieren, lassen sich diese beliebig modifizieren, das heißt mit lokalen Eigenschaften und zusätzlichen Methoden anreichern.
Ein Aspekt hinsichtlich der Konstruktoren sei noch erwähnt. Alle OO-Sprachen bieten die Möglichkeit, den Konstruktor zu überladen und damit alternative Methoden zum Erzeugen eines Objekts anzubieten. In JavaScript ist dies auf direktem Weg nicht möglich: Je Prototyp darf nur ein Konstruktor existieren. Allerdings ist JavaScript nicht strikt in Bezug auf die Zahl der übergebenen Parameter. Es erfolgt nur eine Bindung der tatsächlich spezifizierten Parameter an deren formales Pendant. Auf zusätzlich übergebene Parameter kann ebenfalls zugegriffen werden, jedoch nur über die Parameterliste und nicht über formale Parameter. Für unsere Zwecke von Interesse ist jedoch der Fall, wenn die Zahl der aktuellen niedriger ist als durch die formalen Parameter der Konstruktormethode spezifiziert, denn dann initialisiert der Interpreter die fehlenden Argumente einfach mit undefined. Im Konstruktor kann man über die Eigenschaft length, die durch den Punktoperator an den Namen des Prototyps gebunden ist, ermitteln, wie viele Argumente der Benutzer tatsächlich übergeben hat; die fehlenden lassen sich mit Standardwerten vorbelegen.
Kein Geheimnisprinzip
Im Gegensatz zu seinem Konkurrenten VBScript bietet JavaScript keine Möglichkeit, die Sichtbarkeit beziehungsweise den Zugriff auf Attribute oder Methoden eines Objekts von außen zu beschränken. Das von Parnas postulierte Geheimnisprinzip (Information Hiding) ist hier nicht umgesetzt. Eine Möglichkeit, dies zu kompensieren, besteht darin, in einem Kommentar vor der Definition des Konstruktors die Schnittstelle, also die Menge aller öffentlichen Methoden und Eigenschaften, aufzulisten. Alles Weitere obliegt der Disziplin des Programmierers, der auch ungehinderten Zugriff auf die privaten Attribute besitzt, auch wenn dies nicht in der Intention des Implementierers jenes Objekts lag.
Information Hiding ist jedoch nur eine Seite der MĂĽnze; der andere Aspekt der Kapselung, der zudem weitaus bedeutender ist, bezieht sich auf das BĂĽndeln und Verschachteln von Informationen zu Paketen, und dies unterstĂĽtzt JavaScript, wie der Punktoperator vor Augen fĂĽhrt. Man kann also neben der Vererbung auch in Bezug auf die Kapselung davon sprechen, dass JavaScript objektorientiert ist.
Polymorphie, genauer die Inklusionspolymorphie, bezeichnet die Fähigkeit, eine Botschaft an ein Objekt zu schicken, wobei zur Kompilierzeit noch nicht feststeht, von welcher Klasse ein Objekt abgeleitet ist. Diese Entscheidung kann erst zur Laufzeit getroffen werden, zum Beispiel mit Hilfe einer internen, virtuellen Tabelle, wie es unter C++ gängige Praxis ist. Die Voraussetzungen für Polymorphie sind Vererbung und polymorphe Typen. Letzteres drückt aus, dass ein Objekt mehrere Typen besitzt. In Java sind dies beispielsweise alle, die ein Objekt einer Klasse entlang der Generalisierungshierarchie erbt.
Spricht man von der Existenz polymorpher Typen, so trifft dies in Bezug auf JavaScript nicht ganz den Punkt: Da es keine Typisierung gibt, sind alle Instanzen in gewisser Weise polymorph. Zudem ist späte Bindung unter JavaScript Standard, da stets erst zur Laufzeit ermittelt wird, an welches Objekt eine Methode gebunden wird. Das Problem liegt bei JavaScript an einer anderen Stelle: Die Sprache ist in Bezug auf Polymorphie eher zu wenig restriktiv; deshalb können durchaus Botschaften an einzelne Objekte geschickt werden, die in der Klassenhierarchie beziehungsweise Prototypenkette in keinerlei Relation stehen. Einzige Bedingung: die Instanzen bieten jene Methode an. Somit ist semantische Substituierbarkeit in diesem Kontext nur gegeben, wenn der Programmierer besondere Sorgfalt beim Einsatz von Polymorphie walten lässt und sie nicht wahllos einsetzt.
Wiederum ist man also damit konfrontiert, dass in JavaScript die Klassenhierarchie nicht klar definiert beziehungsweise nicht leicht erkennbar ist. Um sie dennoch kenntlich zu machen, kann man beispielsweise eine Funktion instanceOf (obj, proto) schreiben, die eine Mimik des gleichnamigen Operators unter Java darstellt. Für den Internet Explorer existiert jener Operator tatsächlich, unter Netscape muss er jedoch noch geschrieben werden. Die folgende Lösung ist Browser übergreifend und basiert auf einer simplen Vorgehensweise: Die Funktion durchläuft einfach die Prototypenkette des Objekts auf der Suche nach proto; sie liefert true, falls die Suche erfolgreich verlief, und false im erfolglosen Fall (siehe Listing 3).
Listing 3: instanceOf
function instanceOf (obj, proto) {
if (document.all)
return (eval ("obj instanceof proto"));
while (obj != null)
if (obj == proto.prototype)
return true;
else obj = obj.__proto__;
return false;
}
Von UML nach JavaScript
Die eben definierte Funktion findet sogleich Anwendung bei der Umsetzung eines Entwurfs von UML nach JavaScript. Das in Abbildung 1 dargestellte Diagramm zeigt einen solchen, der beispielsweise den Aufbau eines GUI repräsentieren könnte: Es enthält einzelne grafische Objekte, die ein Container umgibt. Container können ihrerseits Container enthalten, da auch sie letzten Endes nur grafische Objekte verkörpern. Natürlich ist dieses Modell rudimentär, aber es genügt, die Konzepte der Objektorientierung unter JavaScript an einem praktischen Beispiel zu demonstrieren. Dem Schema liegt das Composite Design-Pattern zu Grunde.
Listing 4 zeigt die Umsetzung in JavaScript; außerdem enthält es einen Testfall und präsentiert das zugehörige Ergebnis in einem Kommentar. Die Schnittstelle jeder Klasse beziehungsweise jedes Prototyps ist in einer Java-ähnlichen Notation in einem Kommentar angegeben. Das Nachvollziehen des Quelltexts dürfte auch ohne weitere Erläuterungen keine Probleme bereiten.
Listing 4: im HTML-Head eingebettets JavaScript
<html>
<head>
<script language = "JavaScript">
/* -----
Hilfsfunktion
*/
function instanceOf (obj, proto) {
if (document.all)
return (eval
("obj instanceof proto"));
while (obj != null)
if (obj == proto.prototype)
return true;
else
obj = obj.__proto__;
return false;
}
/* -----
abstract class Item {
abstract void print ();
}
*/
function Item () {
this.print = printItem;
}
function printItem () {
}
/* -----
public class Container {
public String label;
public Item [] ItemList;
public void add (Item i);
public void print ();
}
*/
function Container (label) {
this.label = label;
this.ItemList = new Array (0);
this.add = addItem;
this.print = printContainer;
}
function addItem (i) {
if (!instanceOf (i, Item))
return;
else {
var len = this.ItemList.length;
temp = new Array (len + 1);
for (j = 0; j < len; j ++)
temp [j] = this.ItemList [j];
temp [len] = i;
this.ItemList = temp;
}
}
function printContainer () {
document.write ("[Container " + this.label + ": ");
for (j = 0; j < this.ItemList.length; j ++)
this.ItemList [j].print ();
document.write ("]");
}
Container.prototype = new Item ();
/* -----
public class Shape {
public String label;
public void print ();
}
*/
function Shape (label) {
this.label = label;
this.print = printShape;
}
function printShape () {
document.write ("[Shape " + this.label + "]");
}
Shape.prototype = new Item ();
/* -----
Testfall
*/
var shape1 = new Shape ("Triangle");
var shape2 = new Shape ("Circle");
var shape3 = new Shape ("Rectangle");
var cont1 = new Container ("Outer");
var cont2 = new Container ("Inner");
cont1.add (shape1);
cont1.add (cont2);
cont2.add (shape2);
cont2.add (shape3);
cont1.print ();
/* -----
Ergebnis
[Container Outer:
[Shape Triangle]
[Container Inner:
[Shape Circle]
[Shape Rectangle]
]
]
*/
</script>
</head>
</html>
Ohne Frage unterscheidet sich die Objektorientierung in JavaScript von anderen OO-Sprachen. Dies bedeutet jedoch nicht, dass der Weg, den diese Skriptsprache beschreitet, weniger objektorientiert oder schlechter ist. Im Gegenteil: Das Prototypenkonzept dürfte in Zukunft noch von sich reden machen. In jedem Fall ist die Andersartigkeit weniger ein Nachteil als eher eine neue Facette, die sich eröffnet und der man sich nicht verschließen sollte.
Cai Ziegler
studiert Informatik an der Universität Passau und arbeitet als Entwickler und Consultant für die DD Systems GmbH in Frankfurt/Main.
Literatur
[1] Alexander Nakhimovsky, Tom Myers; JavaScript Objects; Birmingham (Wrox Press) 1998
[2] Michael P. Wagner; Selbst ist das Objekt; Self: objektorientierte Entwicklung in der Public Domain, iX 6/1993, S. 156
iX-TRACT
- JavaScript unterstĂĽtzt Objektorientierung, vertritt jedoch nicht den klassenbasierten, sondern einen auf Prototypen fuĂźenden Ansatz.
- Prototypen sind zwar mächtig, implizieren jedoch zugleich eine Verwässerung der klar definierten Klassenhierarchie.
- Die fehlende Typisierung von JavaScript ist Voraussetzung fĂĽr die Realisierung des Prototypenkonzepts.
(hb)