Homepagebau
Dateiversion:
diesen Beitrag diskutieren,
ergänzen, eine Frage stellen
Menü einblenden
 

Das Prototypobjekt

Schwierigkeitsgrad:

 
Beispiel für JavaScript - Klasse downloaden


ZIP-Archiv
5 kbyte

Beispiel ansehen

Beschreibung: diese Klasse implementiert einen Binärbaum (Binary Tree) in JavaScript. Binärbäume verwalten Daten als Zuordnung von Schlüssel->Wert, ähnlich wie Hashtables. Allerdings werden die Werte in sortierter Reihenfolge gespeichert.

 

Einführung

Grundbegriffe: Klasse, Objekt, Eigenschaft

Auch wenn man im Allgemeinen in Bezug auf JavaScript von "Klassen" spricht, ist die Bezeichnung nicht völlig zutreffend. Der Begriff der "Klasse" wurde stark durch Programmiersprachen wie Java, C++ oder C# geprägt. Diese Sprachen sind streng typisiert. Dort versteht man unter einer Klasse in erster Linie eine Art Schablone, nach der Objekte angelegt werden können. Ein nach dieser Schablone gestaltetes Objekt nennt man eine "Instanz" seiner Klasse. Tatsächlich sind alle Objekte welche sie in Java, C++ oder C# verwenden immer Instanzen irgendeiner Klasse. Selbst dann wenn dies im Einzelnen nicht explizit angegeben ist.
Objekte wiederum sind "Speicherrahmen" (Frames). Sie können sich dies wie eine Art "Box" vorstellen, in welche man Dinge hineinlegt von welchen man meint, dass sie zusammen gehören. Im Sinne einer Programmiersprache sind dies: Methoden, Funktionen, Variablen, Konstanten und - auch wenn das zunächst eigenartig erscheint - andere Objekte.
Alle diese Dinge, welche sie "in einer Box" finden definieren den Inhalt dieser Box. Man sagt: "die Komponenten eines Objektes definieren seine Eigenschaften". Die Komponenten eines Objektes werden daher oft auch als "Eigenschaften" bezeichnet. Üblicherweise schreibt man dies in den meisten Programmiersprachen als Objekt.Eigenschaft oder Objekt->Eigenschaft.

Alle Instanzen einer Klasse enthalten exakt die gleichen Komponenten wie durch ihre Klasse definiert. Tatsächlich unterscheiden sie sich ausschließlich in der Belegung ihrer Eigenschaften. Die Zahl, die Benennung und der Typ der Eigenschaften ist aber durch die Klasse festgelegt.

 
Grundbegriffe: Aliasing, Werttypen, Referenztypen (value type vs. reference type)

Beachten sie bitte, dass ein Objekt etwas anderes ist als eine Variable. Variablen sind "Werttypen", während Objekte "Referenztypen" sind.

Sie wissen vielleicht bereits, dass eine Variable ein konkreter Name für einen Speicherbereich ist. Der Typ einer Variable gibt an, wie groß der Speicherbereich ist und wie der Inhalt zu interpretieren ist. Zum Beispiel als Ganzzahl (Integer), als Ganzzahl mit doppelter Genauigkeit (Double) oder als Textzeichen (Character). Ein Zugriff auf einen Werttyp resultiert direkt in einem Zugriff auf den Wert der Variable.

Eventuell haben sie bereits C++ programmiert und kennen den Begriff der Referenz daher als "Zeiger" oder "Pointer". Als Referenztypen bezeichnet man Zeiger auf Speicherrahmen. Das bedeutet, der Zugriff auf einen Werttyp resultiert nicht direkt in einem Zugriff auf die im Speicherrahmen gespeicherten Werte sondern lediglich in einem Zugriff auf die Referenz auf diesen Speicherrahmen.

Beispiel:

(Werttyp) a = 1;
(Werttyp) b = 0;

=> a=1 und b=0

a = b; // a = 0
b = b +1;

=> a=0 und b=1

(Referenztyp) A = new Referenz(1);
(Referenztyp) B = new Referenz(0);

=> A=1 und B=0

A = B;
B-> = B-> +1;

=> A-> = 1 und B-> = 1 und A=B

Dieser Quellcode ist in keiner echten Programmiersprache geschrieben. Wichtig ist hier nicht der angedeutete Pfeil '->'. Der entscheidende Punkt ist: die Werttypen 'a' und 'b' haben nach der Zuweisung 'a=b' den gleichen Wert, haben aber ansonsten nichts mit einander zu tun. Die Referenztypen 'A' und 'B' haben nach der Zuweisung 'A=B' ebenfalls den gleichen Wert, ABER dieser Wert ist weder 0 noch 1 sondern eine Referenz auf die Speicherzelle in der dieser Wert gespeichert ist. Nach dieser Zuweisung haben die beiden Referenzen A und B nicht nur den gleichen Wert, sondern weil A und B Referenzen - also Zeiger - sind verweisen sie auf die gleiche Speicherzelle. Wird nun der Inhalt der Speicherzelle von B geändert, ändert sich auch der Wert von A.

Der Pfeil soll hier andeuten, dass wir nicht den Wert der Referenz meinen, sondern den Wert der Speicherzelle auf welche die Referenz verweist. Analog bezeichnet man den Pfeil '->' als "Dereferenzierungsoperator".

Wenn 2 Referenztypen A und B auf die gleiche Speicherzelle zeigen so nennt man A einen Alias von B. Umgekehrt gilt natürlich auch: B ist ein Alias von A. Das heißt: A und B sind zwei verschiedene Namen für das selbe Objekt. Die Zuweisung 'A=B' bezeichnet man als "Aliasing".

 
Vertiefung: Objekt-Datenfeld Dualismus

An dieser Stelle möchte ich sie zusätzlich mit einem Prinzip bekannt machen, dass als "Objekt-Datenfeld Dualismus" bezeichnet wird. Was bedeutet das? Nun: sie haben bereits gesehen, dass die Komponente eines Objekts über Objekt->Eigenschaft abgerufen werden kann. Die Zeichenfolge -> nennen wir den Dereferenzierungsoperator. Falls sie bereits andere Programmiersprachen verwendet haben kennen sie diesen Operator möglicherweise bereits.

Tatsächlich gibt es einen Zusammenhang zwischen assoziativen Datenfeldern (eng. "Hashtables") und Objekten. Okay, bitte nicht schlagen: an dieser Stelle genügt es für sie zu wissen, dass ein Zusammenhang zwischen Objekten und Hashtables besteht. Sie müssen nicht unbedingt wissen, was ein Hashtable genau ist.

Was sie sich jedoch einprägen sollten ist die Schreibweise in JavaScript und dass das Verhalten von Hashtables und Objekte für den Programmierer sehr ähnlich ist.

Beispiel:

A = new Object(); // Objekt
B = new Array(); // Hashtable

A.wert    = 10;
B["wert"] = 10;

A.wert    = A.wert + 10;
B["wert"] = B["wert"] + 20; 

A.name    = "mein Objekt";
B["name"] = "mein Hashtable";

A.wert    = B["wert"] -5;
B["name"] = A.name;

Beachten sie bitte, dass in JavaScript anders als in C++ statt '->' einfach '.' geschrieben wird. Diese Schreibweise finden sie auch in C# und Java.

 
Vertiefung: instanzspezifische versus typspezifische Komponenten

Eine Komponente einer Klasse heißt instanzspezifisch, wenn sie im Speicherrahmen jeder von dieser Klasse erzeugten Instanz gespeichert ist.
Eine Komponente einer Klasse heißt typspezifisch, wenn sie im Speicherrahmen der Klasse selbst, aber nicht im Speicherrahmen jeder einzelnen Instanz gespeichert ist.

Das klingt zunächst reichlich abstrakt. Im Grunde genommen bedeutet es aber nur, dass alle Methoden und Eigenschaften, welche sich auf eine konkrete Instanz beziehen instanzspezifisch sind. Alle Komponenten welche sich auf alle Instanzen gleichzeitig beziehen - also auf gemeinsame Eigenschaften aller Instanzen - heißen typspezifisch.

Nehmen wir zum Beispiel "Fische". Wir wissen dass die Geschwindigkeit mit der ein Fisch schwimmt eine Eigenschaft eines konkreten Fisches ist. Jeder Fisch kann mit einer anderen Geschwindigkeit schwimmen. Also ist die Geschwindigkeit instanzspezifisch. Aber: jeder Fisch hat Schuppen. Es ist keine besondere Eigenschaft eines bestimmten Fisches Schuppen zu haben. Also sind die Schuppen typspezifisch.

In Java und C# werden typspezifische Komponenten mit dem Schlüsselwort static gekennzeichnet. Instanzspezifische Komponenten erhalten keine besondere Kennzeichnung.
In JavaScript ist es genau umgekehrt: instanzspezifische Komponenten werden als prototype gekennzeichnet und typspezifische haben keine besondere Kennzeichnung. Dazu aber später mehr.

 
Vertiefung: Klassen versus Prototypen

Klassen selbst sind bis zu einem gewissen Grad ebenfalls Objekte. Zum Beispiel ist jede konkrete Klasse in Java eine Instanz der Klasse "Class" und somit ein Objekt. Dieses Objekt nennt man "Klassenobjekt". Es wird automatisch angelegt wenn der Quellcode der Klasse ausgeführt wird. Das mag zunächst verwirrend erscheinen, aber: eine Klasse ist quasi eine Schablone für Objekte. Warum sollte die "Schablone" eines Objekts etwas anderes sein, als ein Objekt? Wie wir im letzten Abschnitt gesehen haben, unterscheiden sich alle Instanzen einer Klasse schließlich nur durch die Belegung ihrer Eigenschaften.

Das führt uns zu einer anderen Interpretation von Klassen. Man könnte Klassen mit einer gewissen Berechtigung auch einfach wie ein Objekt definieren und dessen Eigenschaften aufzählen. Dieses Objekt nennen wir den "Prototyp". Wir sind in der Lage eine Kopie des Prototypobjektes anzulegen und so neue Instanzen zu erzeugen. Wir können sogar zwischen instanzspezifischen und typspezifischen Komponenten unterscheiden, indem wir dem Objekt entsprechende Eigenschaften geben. Wenn es so einfach ist, warum schlagen wir uns überhaupt mit Klassen herum?

Es gibt einen wesentlichen Unterschied zwischen Klassen und Prototypen. Eine Klasse ist im wesentlichen beschrieben durch die Klassendefinition: also durch Text. Prototypen sind aber Objekte und können daher nur den Teil des Textes wiedergeben, welcher direkt durch das Objekt repräsentiert werden kann. Bei den meisten Programmiersprachen ist die Klassendefinition als Text aber reichhaltiger als die Darstellung als Objekt.

Zum Beispiel kennen sie vielleicht aus C oder Java die Schlüsselwörter "public" und "private". Auf eine als "private" deklarierte Komponente kann nur innerhalb der Klassedefinition zugegriffen werden. Das heißt, es ist eine interne Komponente der Klasse. Diese ist zwar eine Eigenschaft des Objekts, aber es ist kein direkter Zugriff auf sie möglich.
Um unser Beispiel mit den Fischen erneut aufzugreifen: der Darminhalt eines Fisches ist ganz klar eine Eigenschaft eines Fisches. Aber niemand hat darauf direkten Zugriff außer dem Fisch selbst. Sie können den Darminhalt eines Fisches lediglich beeinflussen, indem sie ihn füttern oder eben nicht.
Dies ist mit einem Objekt nicht realisierbar. Hätte Mutter Natur Fische nach den eingeschränkten Möglichkeiten von Prototypen generiert dann wäre ihr Darminhalt stets und ständig für jedermann greifbar. Ich möchte sie allerdings bitten, sich das nicht bildhaft vorzustellen.

Man muss also sagen: Klassen sind zwar Objekte, aber mit gewissen zusätzlichen Eigenschaften.

Klassen in JavaScript

Vertiefung: Gemeinsamkeiten und Unterschiede des Klassenkonzepts

In JavaScript gibt es kein Schlüsselwort "class" und auch keine Klassen im Sinne von C oder Java. Trotzdem lassen sich die wesentlichen Eigenschaften von Klassen in JavaScript umsetzen und ich werde im Folgenden auch bei JavaScript von "Klassen" sprechen, auch wenn dies aus fachlicher Sicht nicht 100%ig korrekt ist.

Dass es in JavaScript überhaupt so etwas wie "Klassen" gibt, hängt mit dem Objektmodell zusammen. Anders als in Java oder C# sind in JavaScript auch Funktionen Objekte. Zum Beispiel ist die Konstruktorfunktion ein Objekt: und dieses Objekt kann Eigenschaften besitzen. Eigenschaften können wiederum selbst Objekte sein. Also zum Beispiel Werte oder Funktionen. Ein Konstruktor kann als Eigenschaft auch einen weiteren Konstruktor besitzen.

Ergo: wenn wir in JavaScript von Klassen sprechen, dann meinen wir immer den Konstruktor + seine Eigenschaften.
Daraus ergibt sich eine weitere Besonderheit von JavaScript: Klassen in JavaScript haben immer einen öffentlichen Konstruktor. Das ist tatsächlich eine Besonderheit, denn in C# oder Java darf es auch Klassen geben, welche keinen Konstruktor besitzen oder deren Konstruktor nicht öffentlich ist.

Beginnen wir mit einem Beispiel. Wir erzeugen nun eine Klasse "Kreis". Diese Klasse soll einen Konstruktor besitzen um eine neue Instanz zu erzeugen. Jeder Kreis ist definiert durch seinen Radius und eine XY-Position in der Ebene. Zusätzlich speichern wir die Kreiszahl "Pi" als typspezifischen Wert. Außerdem eine instanzspezifische Methode "toString()" welche Radius und Position des Kreises als Text ausgeben soll und eine typspezifische Funktion "equal()", die 2 Kreise vergleicht.

Beispiel:

Klasse "Kreis" in JavaScript

/* In JavaScript werden Klassen durch ihren Konstruktor repräsentiert. */

function Kreis(radius, x, y) {
  this.radius = radius;
  this.x = x;
  this.y = y;
}

/* der Wert "pi" ist typspezifisch. Er ist eine Eigenschaft der Klasse. */

Kreis.pi = 3.141592654;

/* die Methode "toString()" ist instanzspezifisch. Daher ist sie eine Eigenschaft des Prototypobjekts. */

Kreis.prototype.toString = function() {
  return "Das ist ein Kreis mit Radius "+ this.radius +
         " und Position ("+ this.x +","+ this.y +").";
}

/* die Methode "equal()" ist typspezifisch. Daher ist sie eine Eigenschaft der Klasse. */

Kreis.equal = function(A,B) {
  return (A.x==B.x && A.y==B.y && A.radius==B.radius);
}

Zum Vergleich nun das gleiche Beispiel geschrieben in C#. Diese beiden Klassendefinitionen sind äquivalent. An diesem Beispiel sollen sie sehen, welche Unterschiede und Gemeinsamkeiten es in der Schreibweise zwischen beiden Sprachen gibt. Dies ist deshalb so wichtig, weil JavaScript eine Besonderheit in der Welt der objektorientierten Programmierung darstellt und die meisten Sprachen eine Syntax verwenden, welche der von C# sehr ähnlich ist. Achten sie bitte darauf, dass sie in JavaScript auch bei der Definition einer Klasse mit Objekten arbeiten. In JavaScript wird aus der Beschreibung des Klassenobjekts die Klasse abgeleitet. In C# ist es genau umgekehrt: hier wird aus der Beschreibung der Klasse das Klassenobjekt abgeleitet.

Beispiel:

Klasse "Kreis" in C#

public class Kreis{

public int radius;
public int x;
public int y;

public Kreis(int radius, int x, int y) {
  this.radius = radius;
  this.x = x;
  this.y = y;
}

public static int pi = 3.141592654;

public String toString() {
  return "Das ist ein Kreis mit Radius "+ this.radius +
         " und Position ("+ this.x +","+ this.y +").";
}

public static boolean equal(Kreis A, Kreis B) {
  return (A.x==B.x && A.y==B.y && A.radius==B.radius);
}

}

Ebenfalls bemerkenswert ist an dieser Stelle, dass genau wie in Java und C# auch in JavaScript jede Instanz einer Klasse automatisch die Eigenschaft "toString()" besitzt. Selbst dann, wenn sie nicht explizit angegeben wird. In diesen drei Sprachen haben alle Objekte eine gemeinsame Basisklasse mit dem Namen "Object". Die Funktion "toString()" ist eine Eigenschaft dieser Klasse und wird an die Instanzen aller Klassen vererbt. Ebenso wie in den beiden anderen Sprachen ist auch in JavaScript die Funktion "toString()" in ihrer ursprünglichen Form ziemlich nutzlos. Es wird erwartet, dass sie als Programmierer diese Funktion für ihre eigene Klasse auf geeignete Weise überschreiben.

 
Vertiefung: Das Prototypprinzip

Wie gesehen hat ein Konstruktor in JavaScript die besondere Eigenschaft "prototype". Der "Prototyp" ist ein Objekt. Dieses Objekt verwendet die Konstruktorfunktion als Basis zur Erzeugung einer neuen Instanz. Jede neu erzeugte Instanz ist eine exakte Kopie des Prototypobjekts INKLUSIVE der Änderungen welche beim Aufruf des Konstruktors durch den Konstruktor selbst an der neuen Instanz vorgenommen werden.

Beachten sie bitte, dass sie mit Objekten arbeiten! Dem Prototypobjekt können jederzeit zur Laufzeit neue Eigenschaften zugewiesen oder alte Eigenschaften ersetzt werden! Eine Klassenbeschreibung in JavaScript ist nie statisch sondern kann zur Laufzeit des Skripts dynamisch geändert werden.

Dadurch, dass es sich bei einem Prototyp um ein Objekt handelt gibt es keine Unterscheidung zwischen öffentlichen und nicht-öffentlichen Komponenten, denn die Eigenschaften eines Objekts sind selbstverständlich IMMER öffentlich.

Das Prototypobjekt wird von JavaScript IMMER angelegt. Selbst dann, wenn sie es in ihrer Klassendefinition nicht erwähnen.

 
Vertiefung: den Typ eines Objekts bestimmen

Das Prototypobjekt besitzt - ohne dass sie dies explizit angeben müssen - die Eigenschaft "constructor". Diese Eigenschaft ist ein Zeiger auf die Konstruktorfunktion selbst und wird von JavaScript automatisch vergeben. Sie können die Eigenschaft Konstruktor auch mit einer eigenen Referenz überschreiben, falls dies erforderlich sein sollte.

Die Eigenschaft "constructor" wird vom Prototypobjekt an alle Instanzen der Klasse vererbt. Das heißt, sie können diese Eigenschaft verwenden um festzustellen ob 2 Objekte vom gleichen Typ sind:

/* Diese Funktion liefert "true" wenn A und B vom gleichen Typ sind und sonst "false" */

function typcheck (A,B) {
  if (typeof A == "object" && typeof B == "object") {
    return (A.constructor == B.constructor);
  } else {
    return (typeof A == typeof B);
  };
};

Die Eigenschaft "constructor" ist noch zu anderen Dingen zu gebrauchen! Dazu ein kleiner Exkurs: Sie kennen vielleicht auch der Psychologie schon das "Kind-mit-Brezel" Syndrom. Der kleine Sohn sieht ein anderes Kind, dass eine Brezel hat und schreit "Mami ich will auch eine Brezel" und egal welche Versprechungen man dem Kinde macht, keine Brezel ist gut genug. Weder, dass es daheim ganz viele Brezeln gäbe, noch dass es gleich um die Ecke leckere Pfannkuchen zu kaufen gibt. Es muss "genau diese Brezel" sein. Dummerweise können sie dem fremden Kind aber weder die Brezel klauen, noch hat das Kind ein Schild um den Hals auf dem der Name des Brezelbäckers steht. Was tun?
In JavaScript gibt es dieses Problem nicht: jede "Brezel" hat die Eigenschaft "constructor" und damit eine Referenz auf ihren "Bäckermeister". Im Klartext: sie müssen gar nicht wissen von welchem Typ ein konkretes Objekt ist um eine neue Instanz des gleichen Typs zu erstellen...

/* so erzeugen sie für B eine neue Instanz der Klasse, zu welcher das Objekt A gehört, ohne den Typ des Objekts zu kennen */

B = A.constructor.call();

Weil "constructor" eine Referenz auf ein Objekt ist, können sie sich auch die Eigenschaften eines Objekts auflisten lassen. Dies ist insbesondere dann nützlich, wenn sie mit fremden Klassen arbeiten deren Funktionsumfang ihnen noch nicht völlig klar ist...

/* A sei das Objekt dessen Eigenschaften wir ergründen wollen */

document.writeln("Eigenschaften der Instanz");

for (eigenschaft in A) {
  document.writeln(eigenschaft+" => "+A[eigenschaft]+"<br>");
};

document.writeln("Eigenschaften des Klassenobjekts");

for (eigenschaft in A.constructor) {
  document.writeln(eigenschaft+" => "+A.constructor[eigenschaft]+"<br>");
};

Fazit

Fassen wir kurz zusammen: unter einer Klasse in JavaScript versteht man den Konstruktor dieser Klasse mit all seinen Eigenschaften. Ein Konstruktor ist eine spezielle Funktion. Alle Funktionen in JavaScript sind Objekte und sind von der Basisklasse "Function" abgeleitet. Alle Funktionen erben daher bestimmte Eigenschaften. Der Konstruktor hat die spezielle Eigenschaft "prototype". Der Prototyp eines Konstruktors ist ein Objekt. Das Prototypobjekt dient dem Konstruktor als Schablone zum Erzeugen neuer Instanzen der Klasse. Jede Instanz besitzt automatisch eine Eigenschaft "constructor", welche eine Referenz auf den Konstruktor ihrer jeweiligen Klasse darstellt. Diese Referenz kann dazu verwendet werden, den Typ eines Objekts zu bestimmen, oder zu überprüfen ob 2 Objekte den gleichen Typ haben.

Die Klassendefinition in JavaScript ist dynamisch und kann über ein gesamtes Skript verteilt erfolgen. Das heißt Klassenkomponenten können zur Laufzeit verändert oder erweitert werden. Die Komponenten einer Klasse sind in JavaScript immer öffentlich.

Das war's auch schon für diese kurze Einführung. Ich hoffe sie war recht aufschlussreich. Für weitere Details zum Thema "Klassen in JavaScript" verweise ich sie an die Literatur. Bei konkreten Fragen stehe ich ihnen gern via Mail zur Verfügung...