OOP in Javascript – Teil 1
Mein erster Versuch hier einen Artikel über Vererbung in JS zu schreiben, scheiterte an einem Punkt den ich übersehen hatte. Daher hier eine neue zweite Version. Ich werde den Artikel aber in drei Teile aufsplitten.
- Teil 1. - OOP in Javascript
- Teil 2. - MOC my Object Creator
- Teil 3. - Vergleiche und Benchmarks
OOP in Javascript
In Javascript gibt es an sich keine Klassen, keine Vererbung, keine privaten Eigenschaften. All die Dinge, die eine Objektorientierte Programmierung ausmachen. Trotzdem wird Javascript als OO Sprache bezeichnet. Warum?
Der Konstruktor
Der Grund dafür ist, dass Objekte existieren und diese um Eigenschaften und Methoden erweitert werden können. Entscheidend ist aber das es möglich ist neue Objekte zu erzeugen. Das Schlüsselwort new KonstrukorFunktion() erzeugt ein neues (Funktions)Objekt. Das ist trivial und funktioniert so:
function A() { |
this.prop = 'hallo'; |
this.func = function() {alert(this.prop);}; |
} |
var obj = new A(); |
obj.func(); |
Eine Funktion, die ein Objekt erzeugt heißt Konstruktorfunktion. Die aber im Grunde keinen Unterschied mit einer herkömmlichen Funktion hat. Lediglich die Bedeutung des Schlüsselwort this innerhalb der Funktion ist eine andere. In Javascript ist this immer der Kontext aus dem eine Funktion aufgerufen wurde. Ein Aufruf einer globalen func() entspricht genau genommen window.func(). Das bedeutet, in der Funktion ist window der Kontext und entspricht dem Wert von this.
Bei einer Konstruktorfunktion, die mit new func() aufgerufen wird, ist der Kontext im Prinzip new. Es wird also ein Objekt erzeugt und this zugewiesen. Die Funktion A() in dem Beispiel, wird dann in diesem Kontext des neu erzeugten Objektes aufgerufen. Dabei werden dem Objekt über this, zwei Eigenschaften hinzugefügt. Eine Zeichenkette und eine Funktion. Jedes neu erzeugte Objekt A() erhält diese Eigenschaften. Da die Zuweisung jedesmal auf's neue zur Laufzeit erfolgt, sollte diese Methode um dem Objekt Eigenschaften zu zuweisen, sparsam verwendet werden - sie kostet viel Zeit. Stattdessen ist es sinnvoller, wenn möglich, Funktionen und Eigenschaften über das prototype Objekt zu deklarieren. Doch das läßt sich nicht immer realisieren, wie im nachfolgenden Beispiel.
private Eigenschaften
Das führt uns zum ersten Trick. Um in Javascript private Eigenschaften zu simulieren, werden diese in der Konstruktorfunktion mit var deklariert. Dadurch sind diese nur lokal sichtbar und können auch nur von den in der Konstruktorfunktion definierten Funktionen benutzt werden. Diese Funktionen sind im OOP Sprachgebrauch privilegierte Funktionen, da sie auf private Eigenschaften zugreifen dürfen.
Ein Beispiel für private Eigenschaften
function A() { |
var privat = 'x'; |
this.privilegiert = function() {return privat;}; |
} |
var obj = new A(); |
alert(obj.privilegiert()); // 'x' |
Nach dem gleichen Prinzip lasen sich auch private Funktionen erzeugen, wenn eine Funktion lokal in der Funktion erzeugt wird, ist diese nach aussen nicht sichtbar.
Vererbung - der klassische Weg
Der klassiche Weg um in Javascript eine Vererbungshierachie umzusetzen funktioniert aber in so einem Fall nicht mehr und muss vermieden werden. Der Weg sähe in etwa so aus:
function A() {} |
function B() {} |
B.prototype = new A(); // B erbt von A |
prototype ist eine Art Vorlage oder Template, auf die alle Instanzen des Objektes zugreifen können. Das ist eine prototypische Vererbung.
Das Problem
Das Problem mit dieser Art der Vererbung ist, dass der Konstruktor von A() bei der Zuweisung aufgerufen wird.
Dadurch wird ein Objekt A() zu einem Zeitpunkt erzeugt, wo es noch nicht gebraucht wird und manchmal auch stört. Z.b. wenn die Anzahl der Objekte die erzeugt gezählt werden sollen.
Ausserdem funktioniert der Trick mit den privaten Variabeln nicht mehr, da mit dieser Methode alle Objekte die gleichen privaten Variabeln haben. Ganz besonders aufgepaßt werden muss hier, dass das Objekt A in allen abgeleiteten Instanzen identisch ist, was, wenn man es nicht weiß, zu seltsamen Verhalten und schwer zu findenen Fehlern führen kann.
Ein Beispiel für die klassische Vererbung
Um das Problem zu zeigen hat das Objekt A() ein privates Attribut und ein Paar setter/getter Funktionen. Bei der Zuweisung des Prototypobjekts wird der Konstruktor aufgerufen, nun hat privat in allen Instanzen von B() den gleichen Wert.
function A() { |
var privat = 'x'; |
this.set = function(p) {if(p) privat = p}; |
this.get = function() {return privat}; |
} |
function B() {} |
B.prototype = new A(); // B erbt von A |
var a = new B(); |
var b = new B(); |
a.set(1); |
b.set(2); |
alert(a.get()); // => 2 |
alert(b.get()); // => 2 ! |
Wegen diesem Problemen enstand der Wunsch, eine eigene Möglichkeit der Vererbung in JS einzuführen, wie es auch in vielen sehr interessanten Artikel im Internet beschrieben wird.
Der falsche Weg
Mein erster Versuch einer Lösung scheiterte an einem besonderen Verhalten des Objektes prototype, dass einerseits vermutlich ziemlich genial ist, wie vieles an JS, aber anderseits diese einfache Lösung verhindert.
Meine Idee war es, nur die prototype Objekte zu zuweisen und im Konstruktor dann mit .apply(this) den Kontext für die privaten Eigenschaften herzustellen. Diese Art der Vererbung klappt eigentlich relativ gut.
Ein Beispiel
Das Objekt A() hat die private Eigenschaft und die Methoden von dem Beispiel oben und noch eine prototypische Methode (diese dient später dazu das Problem bei dieser Art der Vererbung zu erkläutern).
function A() { |
var privat = 'x'; |
this.set = function(p) {if(p) privat = p}; |
this.get = function() {return privat}; |
} |
A.prototype.a = function() { return 'A.prototype.a'; } |
function B() { |
A.apply(this, arguments) |
} |
B.prototype = A.prototype; // B erbt von A |
// und eine prototypische Funktion, s.u. |
B.prototype.b = function() {return 'B.prototype.b';} |
var a = new B(); |
var b = new B(); |
a.set(1); |
b.set(2); |
alert( a.get()); |
alert( b.get()); |
Der Konstruktor A() wird hier tatsächlich nur so oft aufgerufen, wie er soll, nämlich insgesamt zweimal. Die private Eigenschaften bleiben bei der Instanz erhalten und auch die prototype Eigenschaften werden vererbt. Eigentlich alles wunderbar, doch eine besondere Eigenschaft von prototype habe ich übersehen.
Im Grunde steckt hinter den ganzen Techniken von JS keinerlei besondere Magie, wir haben es hier mit Objekten und Funktionsreferenzen zu tun. Techniken, die unter JS weit verbreitet sind und die man auch beherschen sollte. Das einzige Objekt, das eine gewisse Magie enthält, ist das prototype Objekt. Normalerweise dient es zur Erzeugung der prototype-chain, der Prototypkette, an der sich entlang gehangelt wird, um Funktionen in der Vererbungshierachie zu finden. Eine besondere Eigenschaft ist, dass jede Eigenschaft/Funktion die dem prototype Objekt zugewiesen wird, sofort allen Instanzen zu Verfügung steht (das ist logisch, wenn man sich klar macht, dass prototype einfach nur eine Eigenschaft ist, in der Javascript nachschaut, ob eine Funktion existiert - wie gesagt, keine Magie). Das ist die geniale Eigenschaft, aber dummerweise gilt diese Regel auch umgekehrt, A erbt auch von B!
Beispiel für A.prototype = B.prototype und umgekehrt
function A() { |
var privat = 'x'; |
this.set = function(p) {if(p) privat = p}; |
this.get = function() {return privat}; |
} |
A.prototype.a = function() { return 'A.prototype.a'; } |
function B() { |
A.apply(this, arguments) |
} |
B.prototype = A.prototype; // B erbt von A |
B.prototype.b = function() {return 'B.prototype.b';} |
var a = new A(); |
alert( a.b()); |
Der Aufruf a.b(), der eigentlich einen Fehler erzeugen müßte, gibt ein B.prototype.b aus. Aus diesem Grund war der erste Entwurf dieses Artikels fehlerhaft. So schön diese simple Art der Vererbung auch ist, sie führt nicht zum richtigen Ziel.
Zum Ziel führt hier nur eine Erweiterung des Function.prototype Objekt bzw. eine Helperfunktion. Denn um alle Bedingungen, die die Vererbung erfüllen soll, umzusetzen, sind ein paar Verrenkungen nötig.
Bedingungen
Folgende Bedingungen sollte eine Vererbung erfüllen.
- Sie sollte Konstruktorfunktionen nutzen
- Die prototypische Vererbung sollte weiter möglich sein
- private Attribute sollen ebenso möglich sein, wie private und privilegierte Methoden
- Ein zugriff auf das "super" Objekt
- Der Konstrukor soll nur so oft aufgerufen werden, wie eine Instanz erzeugt wird.
Die Umsetzung des Zieles erläutere ich in der 2. Folge. Und jetzt abschließend noch ein paar wirklich weiterführende Links, mit interessanten Artikeln zum Thema.
Links
- Object Hierarchy and Inheritance in JavaScript
- hier wird beschrieben, wie die klassische Vererbung in JS funktioniert.
- The Prototype Chain
- ausführliche Erläuterungen über die technischen Hintergründe der Prototypenkette.
- Javascript object prototype
- Eine sehr gute deutschsprachige Erklärung, des prototype Objekts. Das ist ein Artikel im Rahmen einer Serie, die Objekte in Javascript und deren Funktionsweisen, sehr gut!
- Objektorientierte Programmierung in JavaScript
- Eine verbesserte Variante der klassischen Vererbung (auch in Deutsch). Sie erfüllt fast alle Bedingungen, lediglich der Aufruf der Konstruktorfunktion erfolgt einmal zuviel. Wer eine einfache und effektive Vererbung nutzen möchte und mit diesen Nachteil Leben kann, sollte diese Art in's Auge fassen.
- Object Oriented Programming in JavaScript
- Der Artikel zeigt wie die Objekthierachien in Javascript abgebildet werden.
- Object Oriented Super Class Method Calling with JavaScript
- Hier wird der Weg beschrieben, der eine Lösung des Problems zeigt. Die dort beschriebene Methode nutzen auch einige Frameworks, vor allem MooTools setzt auf eine Funktion, die der dort beschriebenen sehr ähnlich ist. Die einzige Kritik, die ich daran habe, ist der dass keine Konstruktorfunktionen und prototypen verwendet werden können. Es muss immer ein Objekt dem Konstruktor Class() übergeben werden, aus dem dann das entsprechende Funktionsobjekt hergestellt wird, mit dem dann die Instanzen erzeugt werden können.
1439 mal gelesen.


2 Kommentare Einen Kommentar hinterlassen »
Kommentare
Freitag 2.Juli 2010: 13:16
Schöner Artikel. Freue mich schon auf die anderen beiden, sofern Du dafür Zeit und Lust findest.
Montag 5.Juli 2010: 13:17
Danke.
Lust schon, der Artikel ist auch schon im groben Entwurf fertig, aber das Thema ist so komplex, dass mir in vielen Dingen schwer fällt es zu formulieren.
Einen Kommentar hinterlassen