http://www.documentroot.net - Tipps, Tricks, Tutorials, Wissenswertes
23. März 2010

Tutorial: Klassen, Prototypen und OOP in JavaScript

Quelle: http://www.documentroot.net/allgemein/js-tutorial-klassen-prototypen-oop

In JavaScript gibt es keine Klassen im konventionellen Sinn. Stattdessen baut die Sprache auf Vererbung durch Prototypen auf. Hier will ich das Prinzip dieser prototypischen Vererbung und dessen Umsetzung in JavaScript erklären. Ich zeige auch, wie sich mit dessen Hilfe eine aus anderen Sprachen vertrautere klassenbasierte Umgebung nachbilden lässt.

Ich setze hier gewisse Vorkenntnisse voraus – wer noch nie etwas von Klassen und Objekten gehört hat, wird hier nix verstehen. Ebenso sollte man einigermaßen mit der Syntax von JavaScript vertraut sein.

Vererbung über Prototypen

Prototypen-Vererbung: jedes Objekt geht aus einem bereits bestehenden hervor

~ Das Konzept von Prototypen ~

In Prototypen-basierten Programmiersprachen gibt es keine Klassen, sondern nur Objekte. Um in solchen Sprachen eine Form der Vererbung umzusetzen, gibt es das Konzept der Prototypen: aus einem bestehenden Objekt wird mit Hilfe einer Methode clone ein neues erschaffen, das alle Eigenschaften erbt und einige neue hinzufügt.
Dabei wird das alte Objekt nicht einfach kopiert und erweitert – das wäre schrecklich ineffizient. Stattdessen werden nur die neu hinzugekommenen Eigenschaften abgespeichert und zusätzlich eine Referenz, aus welchem Objekt das neue hervorgegangen ist. Damit wird das alte Objekt zum Prototyp des neuen.
Wird nun eine Eigenschaft eines solchen Objektes aufgerufen, so wird zuerst nachgesehen, ob dieses die Eigenschaft selbst enthält; falls nicht, wird im Prototypen nachgesehen. Enthält dieser die Eigenschaft auch nicht, so wird in dessen Prototyp weitergesucht. Und so weiter.

Wird in so einer Kette ein Objekt manipuliert, beispielsweise eine neue Eigenschaft hinzugefügt, so erhalten also alle davon abgeleiteten Objekte automatisch ebenfalls diese Eigenschaft. Dies macht Prototypen-basierte Sprachen hoch flexibel und dynamisch.

~ Prototypen in JavaScript ~

In JavaScript gibt es sowohl Prototypen als auch Klassen. Blöderweise funktionieren beide nicht so, wie man das erwarten würde. Was sich die JavaScript-Erfinder dabei gedacht haben, ist mir nicht ganz klar. Glücklicherweise sind die Mechanismen des Klassen/Prototypen-Systems in JavaScript so mächtig, dass man damit sowohl reine Prototypen- als auch Klassen-Vererbung nachbilden kann. Und das empfehle ich auch dringend, denn sonst wird der Code sehr viel verwirrender, als er eigentlich sein müsste. Doch bevor ich zeige, wie man beide Ansätze hübsch nachbildet, will ich erst mal erklären, wie Klassen und Prototypen in JavaScript zusammenhängen und funktionieren.

~ Objekterschaffung ~

Objekte in JavaScript können auf zwei Arten erzeugt werden: mit Hilfe der Klammerschreibweise:

1
2
3
4
var myObject = {
  myProperty: 'hello',
  anotherProperty: 'world'
}

oder mit Hilfe von Konstruktoren. Jede Funktion in JavaScript kann als Konstruktor fungieren:

1
2
3
4
5
function myConstructor() {
  this.myProperty = 'hello';
  this.anotherProperty = 'world';
}
var myObject = new myConstructor();

Wird eine Funktion mit dem Keyword “new” aufgerufen, so wird diese nun als Konstruktor verwendet. JavaScript erschafft dann ein neues Objekt (“eine Instanz des Konstruktors”), das als “this” dem Konstruktor zur Initialisierung übergeben wird.

Konstruktoren werden in JavaScript oft auch Klassen genannt, da sie gewisse syntaktische Ähnlichkeit damit haben. Sie sind aber viel funktionsschwächer als echte Klassen wie z.B. in Java, PHP oder Python. Ich will daher im Folgenden versuchen, JavaScript-”Klassen” ausschließlich als Konstruktoren zu bezeichnen und von Klassen zu reden, wenn dabei echte Klassen gemeint sind.

** Konstruktoren haben eine Prototype-Eigenschaft **

Wenn wir einen Konstruktor (also eine Funktion) definieren,

1
2
3
4
function myConstructor() {
  this.myProperty = 'hello';
  this.anotherProperty = 'world';
}

so erschafft JavaScript automatisch zu dieser Funktion ein neues, leeres Objekt, das in myConstructor.prototype abgespeichert wird.
Somit besitzt also jede Funktion in JavaScript eine Prototype-Eigenschaft. Diese sollte man einfach nur als eine simple Eigenschaft des Konstruktors verstehen. Sie gewinnt erst Bedeutung, wenn Objekte von diesem Konstruktor erzeugt werden.

** Objekte haben Prototypen **

In JavaScript besitzt jedes Objekt eine interne Referenz zu dessen Prototyp. Leider ist es nicht möglich, auf diese Referenz zuzugreifen oder sie zu manipulieren. Deshalb gilt: ist Objekt A einmal Prototyp von Objekt B, so wird dies immer so sein.
Wenn ich im folgenden von dieser internen Prototyp-Referenz rede, so schreibe ich immer “der Prototyp von Objekt xyz”.
Meine ich dagegen die Eigenschaft “prototype” eines Konstruktors, so schreibe ich “die Prototype-Eigenschaft” oder “myConstructor.prototype”.

** Konstruktoren setzen den Prototyp eines Objekts **

Erschaffen wir mit Hilfe eines Konstruktors ein neues Objekt

1
var myObject = new myConstructor()

so setzt diese Zuweisung den Prototyp (also die interne Prototyp-Referenz) des neuen Objektes myObject auf myConstructor.prototype.

~ Vererbung ~

Die letzten drei Abschnitte waren mit Sternchen gekennzeichnet, da sie absolut zentral fürs weitere Verständnis sind. Also macht bitte erst weiter, wenn ihr diese drei Tatsachen verinnerlicht habt.
Als Nächstes wollen wir uns ansehen, wie man daraus ein Vererbungs-Konzept bauen kann:

1
2
3
4
5
function A() { this.x = 5; }
function B() { this.y = 6; }
B.prototype = new A();
var b = new B();
alert(b.x); // => 5

In diesem Beispiel hat das Objekt b eine Eigenschaft von A geerbt. Das Geheimnis liegt wie schon vermutet in der Zuweisung

1
B.prototype = new A();

Hier wird die Prototype-Eigenschaft von B überschrieben, und zwar mit einem Objekt des Konstruktors A. Wird nun mittels new B() ein neues B-Objekt instantiiert, so bekommt dieses als Prototypen B.prototype, was ja ein Objekt vom Typ A ist. Wird also am Ende die Eigenschaft b.x aufgerufen, so wird zuerst nachgesehen, ob b diese Eigenschaft besitzt. Dies ist nicht der Fall, also sieht JavaScript im Prototyp von b nach. Dieser ist ja ein Objekt vom Typ A, welches die Eigenschaft x besitzt.

~ Nochmal zur Sicherheit ~

Wandeln wir den Code mal ein kleines bisschen ab und sehen, was passiert:

1
2
3
4
5
function A() { this.x = 5; }
function B() { this.y = 6; }
var b = new B();
B.prototype = new A();
alert(b.x); // => undefined

Was ist hier nun geschehen? Zum Zeitpunkt, als b erschaffen wurde, war B.prototype noch unberührt, also ein leeres Objekt. Also bekam b dieses leere Objekt als Prototyp zugewiesen. Erst danach wurde B.prototype durch ein neues Objekt ersetzt, was allerdings keinerlei Auswirkungen auf den Prototypen von b hat. Denn wie schon erwähnt: ist der Prototyp eines Objektes einmal gesetzt, so kann er nie wieder neu gesetzt werden.

~ Das Vererbungsprinzip als Diagramm ~

So eine ganze Vererbungshierarchie analog zu dem Beispiel von eben kann ganz schön komplex werden, daher habe ich hier mal in einem Diagramm aufgemalt, wie man sich das Ganze vorstellen sollte:
Vererbung in JavaScript
Man sieht hier deutlich, wie die Prototypenkette (gestrichelte Linien) bis zu Object.prototype zurückverfolgt werden kann, um eine Eigenschaft oder Methode zu finden.
(Der Konstruktor Object und somit auch Object.prototype sind von JavaScript vordefiniert.)

~ Erweitern von Prototypen ~

Im Beispiel vorhin haben wir die Prototype-Eigenschaft von B neu gesetzt. Dies hatte keine Auswirkungen auf bereits existierende Instanzen von B. Stattdessen könnte man auf die Idee kommen, B.prototype zu manipulieren, anstatt komplett neu zu setzen:

1
2
3
4
function B() { this.y = 6; }
var b = new B();
B.prototype.x = 4;
alert(b.x); // => 4

Das Ergebnis kommt folgendermaßen zu Stande: das Objekt b wird zu dem Zeitpunkt erschaffen, als B.prototype noch ein leeres Objekt ist. Also erhält b dieses als Prototyp. Danach wird B.prototype die neue Eigenschaft hinzugefügt. Da B.prototype dadurch aber immernoch das selbe Objekt ist, zeigt die interne Referenz von b nach wie vor darauf. Wird nun also b.x aufgerufen, so findet JavaScript den gesuchten Wert im Prototypen von b.

In diesem Beispiel haben wir mit Hilfe der Prototypen-Maschinerie ein bestehendes Objekt nachträglich erweitert – und genau dies ist eine der großen Stärken von JavaScript. Die Datentypen sind nicht statisch, sondern können zu beliebigen Zeitpunkten über die Prototype-Eigenschaft erweitert werden. Die JavaScript-Frameworks Prototype und MooTools machen beispielsweise von dieser Technik Gebrauch, um nativen JavaScript-Datentypen neue Methoden hinzuzufügen. Wollen wir beispielsweise jedem String eine Methode “reverse” verpassen, so könnten wir das mit dieser Technik bewerkstelligen:

1
2
3
4
String.prototype.reverse = function() {
  return this.split("").reverse().join("");
}
alert('hello world'.reverse()); // => 'dlrow olleh'

Diese Technik werde ich in den weiteren Abschnitten ebenfalls verwenden, um konventionelle Vererbungsmechanismen nachzurüsten.

~ Reine Prototypen-Vererbung in JavaScript ~

Nun wollen wir uns das Wissen der vorherigen Abschnitte zu Nutze machen, um eine kleine Methode zu schreiben, die uns reine Prototypen-Vererbung ermöglicht:

1
2
3
4
5
6
7
Object.prototype.clone = function(extension) {
  function fakeConstructor() {};
  fakeConstructor.prototype = this;
  var obj = new fakeConstructor();
  for (field in extension) obj[field] = extension[field];
  return obj;
}

Die Funktion clone erschafft aus einem bestehenden Objekt ein neues, welches alle Eigenschaften erbt und zusätzlich noch einige neue definieren kann:

1
2
3
4
5
6
7
var a = {x: 5};
var b = a.clone({y: 6});
var c = b.clone({z: 7});
alert(b.x); // => 5
alert(b.y); // => 6
alert(c.x); // => 5
alert(b.z); // => undefined

Diese Form der Vererbung ist kurz, prägnant und vor allem dann von Nutzen, wenn ein ganzes Klassen-Framework Overkill wäre.

~ Reine Klassen-Vererbung in JavaScript ~

Als nächstes bilden wir Klassen nach, ähnlich wie in anderen Sprachen:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Class(obj) {
  // Baue Konstruktor, der die angegebenen Eigenschaften in der Prototype-Eigenschaft erhält.
  var Constructor = function() {
    if (this.initialize) this.initialize.apply(this, arguments);
  }
  Constructor.prototype = obj;
  return Constructor;
}
 
Function.prototype.mixin = function(obj) {
  // Erweitere die Prototype-Eigenschaft einer Klasse um ein paar Funktionen.
  for (field in obj) this.prototype[field] = obj[field];
  return this;
}
 
Function.prototype.extend = function(obj) {
  // Baue aus einer bestehenden Klasse eine neue mit erweiterter Prototype-Eigenschaft.
  return Class(new this).mixin(obj);
}

Hier ein Anwendungsbeispiel:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
A = Class({
  name: 'A',
  initialize: function() { this.x = 5; }
});
 
B = A.extend({
  name: 'B',
  greet: function() { alert('hello world!'); }
});
 
C = B.extend({
  name: 'C'
});
 
var a = new A(); var b = new B(); var c = new C();
alert(c.x); // => 5
alert(b.name); // => 'B'
c.greet() // => 'hello world'
a.greet(); // => error

Diese Klassen-Funktionen sind sehr minimal gehalten – es ist beispielsweise keine Möglichkeit vorgesehen, um aus einer Klasse dynamisch auf die Basisklasse zuzugreifen. Wer an einem vollständigeren (und komplexeren) Klassen-Framework interessiert ist, wird unter anderem in den MooTools fündig.

~ Fazit ~

Das ganze Konzept der Prototypen/Klassen-Vererbung in JavaScript zu kapieren, hat mich einige Zeit gekostet. Und so wird es dem Leser hier vermutlich auch gehen. Daher möchte ich nochmal hervorheben, dass es äußerst wichtig ist, die Sternchen-Absätze zu verstehen. Wer diese verinnerlicht hat, wird nach einigen eigenen Experimenten das Konzept verstanden haben und sollte dann auch in der Lage sein, meine Klassen-Funktionen selbst zu reproduzieren.

Allgemein, Internet, Software-Entwicklung , , , , , , , , , ,

32w