Javascript ist Toll!

8. Februar 2010 - 11:23

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.

  1. Teil 1. - OOP in Javascript
  2. Teil 2. - MOC my Object Creator
  3. 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.

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

Comments (12)
14472 mal gelesen.

12 Kommentare

direkt zum Formular »

Seiten:

1. Kommentar von: RedTuesday
2. Juli 2010: 13:16

Schöner Artikel. Freue mich schon auf die anderen beiden, sofern Du dafür Zeit und Lust findest. :)

    2. Kommentar von: Struppi
    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. Pingback von: Tweening – Effekte mit Javascript [Javascript ist Toll!]
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 […]

4. Kommentar von: QuHno
13. September 2010: 23:16

Yahoo ist auch immer wieder lesenswert:
A JavaScript Module Pattern – hilft den Namespace sauber zu halten :)

5. Kommentar von: Tim
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).

6. Kommentar von: Struppi
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.

7. Kommentar von: Steffen Müller
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

8. Kommentar von: paul
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:

function A_Interface() {
  this.helloWorld = function() {
    throw "Missing overwrite method in implementation for A_Interface.helloWorld()."
  }
}

B_Implentierung.prototype = new A_Interface();
B_Implentierung.prototype.constructor = B_Implentierung;
function B_Implentierung() {
  this.helloWorld = function() {
    alert('Hello World');
  }
}
9. Kommentar von: Struppi
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.

10. Kommentar von: paul
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

11. Kommentar von: Struppi
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:

function object(base) {
   function Proto() {}
   Proto.prototype = base;
   return new Proto();
}

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.

12. Pingback von: EaselJS: Lift-off und OOP | crusy.net
7. Januar 2012: 13:59

[…] objektorientiertes Programmieren in JS? Meine JS-Kenntnisse sind etwas eingerostet, aber hier gibt es eine nette Basic- (!) […]

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

Powered by WordPress Stop Spam Harvesters, Join Project Honey Pot
marketing-bankruptcy
marketing-bankruptcy
marketing-bankruptcy
marketing-bankruptcy