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.
ähnliche Artikel
- 22. Juni 2012 -- Tween(2) (0)
Die Technik um mit Javascript eine Bewegung zu animieren...
- 15. November 2011 -- Round Robin Berechnung mit Javascript (0)
Mit dem Round Robin Algorithmus können Spielpaarungen...
- 3. September 2010 -- Tweening – Effekte mit Javascript (4)
Tweening und Transition sind zwei Begriffe aus der...
14472 mal gelesen.
12 Kommentare
direkt zum Formular »
Seiten:
2. Juli 2010: 13:16
Schöner Artikel. Freue mich schon auf die anderen beiden, sofern Du dafür Zeit und Lust findest. :)
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.
3. September 2010: 13:28
[…] warten einige Leser auf die Fortsetzung meiner Reihe OOP mit Javascript. Ich bin während der Recherche zum 2. Teil auf einige Aspekte gestossen, die mein ursprüngliches […]
13. September 2010: 23:16
Yahoo ist auch immer wieder lesenswert:
A JavaScript Module Pattern – hilft den Namespace sauber zu halten :)
2. Januar 2011: 21:25
Vorsicht an alle die wissen wollen wie Vererbung in JS funktioniert, dieser Artikel ist eher ein Tagebucheintrag, er endet mit einem Beispiel wie es nicht geht, jedoch bietet keine Lösung! (Evt. kann der Author das auch am Anfang klarstellen).
3. Januar 2011: 13:08
Ein Blogeintrag ist typischerweise immer wie ein Tagebucheintrag. Aber ich hatte auch am Anfang geschrieben, dass der Artikel noch zwei weitere Folgen haben wird.
Das Problem ist, dass die prototypische Vererbung, die ich in dem Artikel auch kurz erkläre, die Paradigmen, die ich gerne umsetzen wollte, nur über Umwege möglich macht, die alle ihre Nachteile haben. Den Weg, den ich gefunden habe, bin ich momentan noch am erproben, ich bin aber kurz vor der Fertigstellung des zweiten Teils.
Interessant finde ich, dass die Zuweisungen der Prototypeigenschaften über kreuz funktioniert. Eine Eigenschaft, die ich bisher nirgends erklärt gefunden habe. Deshalb ist die in dem Artikel beschriebene „Lösung“ gar nicht so uninteressant wie es scheint. Denn dieses Verhalten läßt sich nutzen, um Objekte zu synchronisieren. Durch die Erweiterung des Prototyps A, wird automatisch auch B erweitert.
Zudem habe ich eine Reihe von Artikel aufgelistet, die sehr ausführlich die Prototypische Vererbung erklären.
18. Februar 2011: 19:04
Hallo zusammen!
Ich habe vor ein paar Tagen einen Workshop zum Thema OOP in Javascript gemacht (naja, und noch ein paar andere Themen: Closures, Scopes, this, Performance…). Die optisch ganz ansprechende Prezi Präsentation steht jetzt online. Als weiterführendes „Präsentierfutter“ also vielleicht ganz interessant:
http://www.incloud.de/blog/2011/02/programming-professional-javascript-while-staying-sane
Viele Grüße,
Steffen
6. September 2011: 13:41
Was sagt mir dieser Artikel außer das Vererbung Scheiße ist weil ich keine gemeinsamen Attribute nutzen kann, was ja der Sinn an Vererbung ist. Funktionen die somit auch gemeinsame Attribute NICHT nutzen können kann ich auch auslagern.
Ich nutze Vererbung meist nur um Interfaces zu simulieren indem die Funktionen des Interfaces nur Fehler werfen und man somit gezwungen ist diese in der erbenden Klasse zu implementieren:
6. September 2011: 13:56
Was sagt mir dieser Artikel außer das Vererbung Scheiße ist weil ich keine gemeinsamen Attribute nutzen kann, was ja der Sinn an Vererbung ist. Funktionen die somit auch gemeinsame Attribute NICHT nutzen können kann ich auch auslagern.
Nein, darum geht es in dem Artikel nicht. Ich schildere darin, dass der klassische Weg der Vererbung über prototype = new Base() schlecht ist, weil du den Konstruktor von Base() an einer Stelle aufrufst wo er nicht hingehört.
Darüber hinaus zeigt er noch den Mechanismus durch das zuweisen von zwei prototypen. Was z.b. auch Jquery.plugin nutzt.
Ob man Interfaces mit JS simulieren möchte, ist Geschmacksache, da JS im Gegensatz zu Java keine Typisierte Sprache ist, ist der Nutzen nur gering. Denn ob jetzt deine Implementierung einen Fehler wirft oder JS an sich, dürfte im Endeffekt egal sein.
6. September 2011: 18:29
Ah ok, wenn ich Dich richtig verstehe lass ich den Konstruktoraufruf einfach weg und dann kann ich in meinem erbenden Objekt eigene Werte zuweisen. Also „B.prototype = A.prototype“ statt „B.prototype = new A()“. Das werde ich mal testen.
Ja klar hast du recht mit der Typisierung. Im Endergebnis tut sich da nichts da man den Fehler erst zur Laufzeit bemerkt. Allerdings dann mit einer besseren bzw. eigenen Fehlerbeschreibung.
Der Hauptgrund ist jedoch das ich in meinen Kommentaren immer auf das „Interface“ verweisen kann. So muss ich nicht immer die Kommentare anpassen wenn sich eine gemeinsame Schnittstelle ändert oder es eine neue Implementierung gibt. Andernfalls müsste man in den Kommentaren jede gültige Implementierung listen oder eine Anleitung schreiben welche Funktionen das erwartende Objekt können muss. Außerdem kann ich über die String suche feststellen wer alles von meinem „Interface“ erbt und somit die fehlenden Funktionen ergänzen.
Der Entwickler wird durch das „Interface“ leider nicht gezwungen dessen Funktionen zu implementieren es bringt aber, in großen Projekten, eine Menge Übersicht und senkt die Fehleranfälligkeit.
Für mich ist es viel mehr eine praktische als eine geschmackliche Frage.
Gruß
Paul
7. September 2011: 8:29
Jein, die Zuweisung der prototype Eigenschaften hat einen unerwünschten Nebeneffekt. Den ich auch im obigen Artikel unter der Überschrift „der falsche Weg“ beschrieben habe.
Der bessere Weg, eine Vererbung mit JS umzusetzen, ist eigentlich Thema von Teil 2 dieses Artikels – aber wer weiß wann ich den fertig kriege. Letztlich läuft es aber immer auf den von Crockford beschriebenen Weg über eine solche Fabrikfunktion hin:
Genaueres hier http://javascript.crockford.com/prototypal.html
Du sagst zwar selbst es ist eine Geschmacksfrage. Aber bei deinem Vorschlag hast du das Problem, dass die Fehlermeldung nicht auf die Stelle wo der Fehler entstanden ist zeigt, sondern dorthin wo du das throw machst. Du erschwerst also das debuggen.
7. Januar 2012: 13:59
[…] objektorientiertes Programmieren in JS? Meine JS-Kenntnisse sind etwas eingerostet, aber hier gibt es eine nette Basic- (!) […]