OOP mit JavaScript
Inhaltsverzeichnis |
Einleitung
Deklaration von Klassen
Folgende Klasse repräsentiert einen Punkt im zweidimensionalen Raum.
function Point2D( x, y ) { this.x = x ? x : 0; this.y = y ? y : 0; }Der Ausdruck
<argument> ? <argument> : 0;sorgt dafür, dass die Variablen auch dann richtig initalisiert werden, wenn keine Argumente übergeben werden. (Default-Konstruktor).
Eine JavaScript-Klasse sieht aus wie eine gewöhnliche Funktion. Diese Funktion übernimmt aber gleichzeitig die Aufgabe einer Klassen-Deklaration und die eines Konstruktors.
Die Erzeugung einer Instanz und der Zugriff darauf geschieht wie folgt:
var p = new Point2D( 10, 15 ); alert( "x: " + p.x + ", y: " + p.y ); // x: 10, y: 15
Im Vergleich zur Deklaration erscheint die Instanziierung dann aber eher so, wie man es erwarten würde.
Methoden
Insgesamt werden drei Möglichkeiten genannt, einer Klasse Methoden hinzuzufügen:
Zuweisung von Funktionen im Konstruktor
function Point2D( x, y ) { this.x = 0; this.y = 0; this.set = function( x, y ) { this.x = x; this.y = y; } this.set( x ? x : 0, y ? y : 0 ); }
In diesem Fall wird im Konstruktor eine set()-Methode gesetzt und direkt aufgerufen. Letztendlich ist das nichts anderes als die Initialisierung einer Funktionsvariablen.
Diese Funktion ist nun auch "von außen" aufrufbar:
var p = new Point2D( 1, 3 ); p.set( 3, 1 ); alert( "x: " + p.x + ", y: " + p.y ); // x: 3, y: 1
Deklaration von Methoden über "prototype"
Eine etwas übersichtlichere und empfohlene Möglichkeit ist die nachträgliche Zuweisung von Methoden:
function Point2D( x, y ) { this.x = 0; this.y = 0; this.set( x, y ); } Point2D.prototype.set = function( x, y ) { this.x = x ? x : 0; this.y = y ? y : 0; } var p2 = new Point2D; p2.set( 30, 21 );
In diesem Beispiel wird eine function "set" dem prototype der Point2D-Klasse hinzugefügt und kann auch im Konstruktor aufgerufen werden. Mit Ausnahme der Tatsache, dass prototype-Funktionen nicht auf private Member zugreifen können, gibt es keinen Unterschied zu den Funktionen, die direkt im Konstruktor erzeugt werden.
Private Member und Methoden
Private Member werden wie lokale Variablen deklariert. Der Zugriff kann nur über Methoden erfolgen, die direkt im Konstruktor zugewiesen werden:
function Point2D( x, y ) { var m_x = x ? x : 0; var m_y = y ? y : 0; this.getX = function(){ return m_x; } this.setX = function( x ){ m_x = x; } this.getY = function(){ return m_y; } this.setY = function( y ){ m_y = y; } }
Die Klasse Point2D enthält zwei private Membervariablen, auf die nur mit den get/set-Methoden zugegriffen werden kann.
Wie man sehen kann, wird auf diese (eigentlich lokalen) Variablen nicht über this zugegriffen. Das ist auch der Grund, warum andere Methoden, die nicht im Konstruktor erzeugt wurden, keinen Zugriff auf diese privaten Member haben:
Point2D.prototype.toString = function() { return "x: " + m_x + ", y: " + m_y; // schlägt fehl (m_x: undefined, m_y: undefined) return "x: " + this.m_x + ", y: " + this.m_y; // schlägt fehl (this.m_x: undefined und this.m_y: undefined) return "x: " + this.getX() + ", y: " + this.getY(); // so funktioniert es }
Vererbung
Mit JavaScript ist es möglich, alle Methoden und Attribute einer Klasse an den prototype einer anderen Klasse zu übergeben:
Point3D.prototype = new Point2D();
Ohne weiteres Zutun hat man nun zweite Klasse, die eine exakte Kopie der ersten darstellt. Nun kann der prototype der zweiten Klasse beliebig verändert werden, ohne Auswirkungen auf die erste Klasse zu haben:
function Point2D( x, y ) { this.x = 0; this.y = 0; this.set( x, y ); } Point2D.prototype.set = function( x, y ) { this.x = x ? x : 0; this.y = y ? y : 0; } Point3D.prototype = new Point2D(); function Point3D( x, y, z ) { this.z = z ? z : 0; this.set( x, y ); } var p = new Point3D( 1, 2, 3 ); alert( "x: " + p.x + ", y: " + p.y + ", z: " + p.z ); // x: 1, y: 2, z: 3
Natürlich könnte nun Point3D eine eigene set()-Methode anbieten, die x, y und z als Parameter erhält:
Point3D.prototype.set = function( x, y, z ) { this.x = x ? x : 0; this.y = y ? y : 0; this.z = z ? z : 0; }
Konstruktoren
Schöner wäre es allerdings, wenn man den Konstruktor der Klasse aufrufen könnte, dessen Member und Methoden man geerbt hat.
Das ist tatsächlich einfach zu realisieren:
Point3D.prototype.set = function( x, y, z ) { Point2D.call( this, x, y ); this.z = z ? z : 0; }
Hier wird diel call()-Methode von Funktions-Objekten aufgerufen und ein Zeiger auf this samt Parameterliste übergeben. this zeigt nun auf ein Point3D-Objekt und wird dem Point2D-Konstruktor untergeschoben. Dieser initialisiert nun den Teil, der an die Point3D-Klasse vererbt wurde.
Es ist empfehlenswert, bei allen übergebenen Parameter davon auszugehen, dass sie auch "undefined" sein kann. Das ist vor allem dann der Fall, wenn die Klasse vererbt wird. (Point3D.prototype = new Point2D);
Mit der Anweisung
this.z = z ? z : 0;
wird die Member-Variable this.z nur dann mit dem Parameter initialisiert, wenn er auch gesetzt ist, ansonsten wird sie mit einem Default-Wert initialisiert.
Virtuelle Methoden
Methoden sind immer virtuell. Anders als zum Beispiel bei C++ können virtuelle Funktionen auch im Konstruktor aufgerufen werden, wobei tatsächlich die Funktion der abgeleiteten Klasse ausgeführt wird. Auch wenn dies nicht immer wünschenswert ist.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <title>OOP mit JavaScript</title> <script type="text/javascript"> function log( str ) { document.f.t.value += str + "\n"; } function Point2D( x, y ) { this.x = x ? x : 0; this.y = y ? y : 0; if( x ) log( this.getAsString() ); } Point2D.prototype.getAsString = function() { return this.toString(); } Point2D.prototype.toString = function() { return "x: " + this.x + ", y: " + this.y; } Point3D.prototype = new Point2D; function Point3D( x, y, z ) { this.z = z ? z : 0; Point2D.call( this, x, y ); } Point3D.prototype.toString = function() { return Point2D.prototype.toString.call( this ) + ", z: " + this.z; } Point4D.prototype = new Point3D; function Point4D( x, y, z, q ) { this.q = q ? q : 0; Point3D.call( this, x, y, z ); } Point4D.prototype.toString = function() { return Point3D.prototype.toString.call( this ) + ", q: " + this.q; } function init() { var p2 = new Point2D( 2, 3 ); var p3 = new Point3D( 2, 3, 5 ); var p4 = new Point4D( 2, 3, 5, 6 ); } </script> </head> <body onLoad="init();"> <form name="f" class="debug"> <textarea name="t" cols=80 rows=15></textarea> </form> </body> </html>
In diesem Beispiel werden drei Klassen implementiert: Point2D, Point3D und Point4D. Point3D erbt von Point2D x und y und Point4D erbt x und y von Point2D indirekt über Point3D und zusätzlich noch z von Point3D.
Im Konstruktor wird jeweils der Vorgänger-Konstruktor explizit aufgerufen, um die geerbten Member zu initialisieren.
Jede Klasse erhält nun eine Methode toString(), die aus den jeweiligen Koordinaten einen String erzeugt und zurück liefert. Auch hier wird Gebrauch von der jeweils geerbten Implementierung gemacht.
Im Konstruktor von Point2D wird nun getestet, wie es sich mit der Virtualität verhält: Wenn der Konstruktor nicht als Default-Konstruktor aufgerufen wird (was zum Beispiel über die AnweisungPoint4D.prototype = new Point3D;geschieht), soll toString() des aktuellen Objekts aufgerufen und das Ergebnis im Textfeld angezeigt werden.
Der Reihe nach werden nun Objekte aller Klassen angelegt und damit jeweils eine Ausgabe ausgelöst:
x: 2, y: 3 x: 2, y: 3, z: 5 x: 2, y: 3, z: 5, q: 6
In diesem Beispiel wurden die eigenen Membervariablen initialisiert, bevor der Vorgänger-Konstruktor aufgerufen wurde, was nicht unbedingt üblich ist. Würde man die Reihenfolge ändern, erhielte man folgende Ausgabe, was gleichzeitig auf das Problem mit virtuellen Methodenaufrufen aus dem Konstruktor heraus hinweist:
x: 2, y: 3 x: 2, y: 3, z: undefined x: 2, y: 3, z: 0, q: undefined
Beim Aufruf von toString() aus dem Konstruktor der Basisklasse sind also noch nicht alle Member-Variablen initialisiert. Aus gutem Grund ist das also in C++ nicht möglich.
Nicht-virtuelle Methoden
Folgendes Beispiel verdeutlicht, dass es in JavaScript keine nicht-virtuellen Methoden gibt:
function log( str ) { document.f.t.value += str + "\n"; } function Shape() { this.showMe = function() { log( "I am a shape!" ); } this.showMe(); } Box.prototype = new Shape; function Box() { this.showMe = function() { log( "I am a box!" ); } Shape.call( this ); } Circle.prototype = new Shape; function Circle() { this.showMe = function() { log( "I am a circle!" ); } Shape.call( this ); } function main() { var s = new Shape; var b = new Box; var c = new Circle; }
Dieses Beispiel ist ähnlich dem mit den PointxD-Klassen. Box und Circle rufen jeweils den Shape-Konstruktor auf, der dann wiederum mit showMe() eine Ausgabe im Textfeld erzeugt. Tatsächlich erscheint in diesem Beispiel drei Mal die Ausgabe "I am a shape!".
Allerdings taucht ein unangenehmes Problem auf, sobald ich in der Funktion main einen weiteren Aufruf hinzufüge:
function main() { var s = new Shape; var b = new Box; var c = new Circle; c.showMe(); }
Die Ausgabe entspricht nicht dem, was beabsichtigt war:
I am a shape! I am a shape! I am a shape! I am a shape!
Tatsächlich wird durch den Aufruf der Vorgängermethode die Variable this.showMe ersetzt.
Würde nun zuerst der Shape-Konstruktor aufgerufen und dann die this.showMe-Funktion der abgeleiteten Klasse ersetzt, hätte das zwar den erwarteten Effekt, aber anschließend wäre die this.showMe-Methode des Shapes in Box und Circle-Klassen einfach nicht mehr aufrufbar.
Statische Methoden und Variablen
Die statischen Methoden einer JavaScript-Klasse werden nicht dem prototype hinzugefügt, sondern der Klasse selbst:
function Static() { } Static.staticVar = 0; Static.staticMethod = function() { Static.staticVar++; alert( "Static.staticVar = " + Static.staticVar ); } Static.staticMethod(); Static.staticMethod();
Statische Methoden können aufgerufen werden, auch wenn keine Instanz der Klasse existiert. Sie werden über den Klassennamen referenziert.
Eine statische Variable existiert nur ein einziges Mal, unabhängig davon, wieviele Instanzen dieser Klasse erzeugt wurden. Auf die statischen Variablen können alle Instanzen zugreifen.
Statische Variablen können zum Beispiel verwendet werden, um die Anzahl der Instanzen einer Klasse zu zählen. Dabei muss allerdings beachtet werden, dass bei der Vererbung einer Klasse ebenfalls eine Instanz erzeugt wird.
Mehrfach-Vererbung
Mit einem kleinen Trick ist auch Multiple Inheritance möglich, auch wenn es in vielen JavaScript-Tutorials anderslautende Meinungen gibt. Um diesen Trick zu verstehen, muss man nur einen Moment über folgenden Ausdruck meditieren:
NewClass.prototype = new OldClass;
Was passiert hier?
NewClass.prototype ist ein Array und der Ergebnis von new OldClass ist ebenfalls ein Array. Folglich wird einfach nur ein Array in ein anderes kopiert.
Warum wird nicht OldClass.prototype kopiert?
Wird der Konstruktor einer Klasse nicht aufgerufen, enthält das Objekt nur alle im Code explizit dem prototype zugewiesenen Funktionen, man würde also nur das Interface kopieren, was unter Umständen durchaus sinnvoll sein könnte.
Nach dem Aufruf des Konstruktors werden implizit über this auch alle Member und im Konstruktor erzeugten Methoden dem prototype zugewiesen.
So kann man sich den Inhalt von prototype jeder Klasse anschauen.
for( m in AnyClass.prototype ) { log( m + ": " + AnyClass.prototype[m] ); }
Erzeugt man nun eine Instanz einer solchen Klasse, kann man sich dessen Inhalt ebenfalls auf diese Weise betrachten:
var anyInstance = new AnyClass; for( m in anyInstance ) { log( m + ": " + anyInstance[m] ); }
Aufgrund dieser Umstände kann man nun die Funktionsweise leicht durchschauen.
Für Mehrfachvererbung müsste es also nun gelingen, den Inhalt mehrerer Klassen in den prototype der neuen Klasse zu übernehmen.
Leider funktioniert NewClass.prototype.concat( new OldClass ) nicht in der gewünschten Weise, da die Methode concat() für prototype nicht existiert.
Daher kann man sich mit einer neuen Methode in der impliziten Basisklasse Object behelfen:
Object.prototype.inherits = function( parent ) { var obj = new parent; for( m in obj ) { this.prototype[m] = obj[m]; } };
Hier wird eine Instanz der parent-Klasse erzeugt und die Inhalte in den prototype übernommen. So lassen sich beliebig viele Klassen erben, wobei die üblichen Probleme mit den Namenskonflikten gleicher Member und Methoden bestehen. In diesem Fall gewinnt immer die zuletzt hinzugefügte Klasse, die alle Member, die mit gleichen Namen bereits existieren überschreibt.
Beispiel:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" > <head> <meta http-equiv="content-type" content="text/html;charset=UTF-8"> <title>OOP mit JavaScript</title> <script type="text/javascript" src="oop.js"></script> <script type="text/javascript"> function log( str ) { document.f.t.value += str + "\n"; } Object.prototype.inherits = function( parent ) { var obj = new parent; for( m in obj ) { this.prototype[m] = obj[m]; } }; function Motor() { this.max_speed = 100; } Motor.prototype.antreiben = function() { log( "antreiben" ); } function Getriebe() { this.max_gear = 4; } Getriebe.prototype.schalten = function() { log( "schalten" ); } Auto.inherits( Motor ); Auto.inherits( Getriebe ); function Auto() { } Auto.prototype.fahren = function() { log( "fahren" ); this.antreiben(); this.schalten(); } function main() { var auto = new Auto; auto.fahren(); log( "\nAuto.prototype:\n"); for( m in Auto.prototype ) { log( m + ": " + Auto.prototype[m] ); } } </script> </head> <body onLoad="main();"> <form name="f" class="debug"> <textarea name="t" cols=80 rows=15></textarea> </form> </body> </html>
Es werden zwei Klassen "Motor" und "Getriebe" deklariert. Beide Klassen sollen in die Klasse "Auto" übernommen werden. Mit den Anweisungen
Auto.inherits( Motor ); Auto.inherits( Getriebe );
wird nun die neue Object-Methode inherits() aufgerufen, die den Auto.prototype jeweils um die Fähigkeiten der geerbten Klassen ergänzt. Damit stehen die Methoden antreiben() und fahren() nun auch dem Auto zur Verfügung.