Objektorientierung

Das Anwendungsfeld von JavaScript meist die Clientseite von Anwendungen. Die Anwendungen sind selten sehr groß. Dennoch ist es sinnvoll, objektorientiert zu entwerfen und zu realisieren.

JavaScript ist eine objektorientierte Sprache, die auf Prototypen basiert. Im Gegensatz dazu ist Java auf dem Konzept der Klassen aufgebaut.

In diesem Teil wird verdeutlicht, wie man in JavaScript mit Objekthierarchien und Vererbung arbeiten kann.

Dazu werden folgende Themen behandelt:

Im Folgenden werden Begriffe am Beispiel der Modellierung einer Angestelltenstruktur verdeutlicht:

Klassen vs. Prototypen

Klassenbasierte objektorientierte Programmiersprachen, wie Java oder C++, sind auf zwei Grundelementen aufgebaut:

Eine prototypenbasierte objektorientierte Programmiersprache, wie JavaScript, kennt diese Unterscheidung nicht. Es existieren einfach nur Objekte. Diese Sprachen kennen Prototypen, dies sind selbst (reale) Objekte, die als Vorlage (template) zur Erzeugung weitere Objekte verwendet werden. Dabei beinhaltet dieser Prototyp alle Eigenschaften, die initial zum Objekt gehören. Jedes Objekt, ganz gleich aus welchem Prototyp es erzeugt wird, kann selbst Prototyp sein und mit zusätzlichen Eigenschaften ausgestattet werden (oder mit weniger).

Klassendefinition

In klassenbasierten Sprachen wird eine Klasse durch eine spezielle Klassendefinition erzeugt. In dieser Definition werden die Konstruktoren zum Bilden von Instanzen spezifiziert, Initialwerte für Eigenschaften  festgelegt und Startroutinen definiert, die bei der Erzeugung einer Instanz ablaufen.  Instanzen werden dann durch new und die Konstruktorfunktion gebildet.

In JavaScript geht das ähnlich, aber man braucht keine eigene Klassendefinition. Jede Funktion kann als Konstruktor Funktion verwendet werden.

Hierarchien

Unterklassen (subclass) und Vererbung (inheritance)

In klassenbasierten Sprachen wird eine Hierarchie von Klassen gebildet, indem man innerhalb der Definition angibt, dass die Klasse eine Unterklasse einer bestehenden Klasse ist. Die Klasse erbt die Eigenschaften ihrer Oberklasse (oder verändert die Eigenschaften) und fügt u.U. neue Eigenschaften hinzu.  Nehmen wir an, employee beinhaltet die Eigenschaften name (Name) und dept (Abteilung). manager ist ein Subklasse von employee mit der zusätzlichen Eigenschaft report. Dann erbt die Klasse manager die Eigenschaften dept und name.

In JavaScript ist Vererbung auch möglich, die Vorgehensweise ist aber unterschiedlich. Zuerst wird ein prototypisches Objekt mit einer Konstruktorfunktion erzeugt und zum Erzeugen eines neuen Objektes verwendet. Im Beispiel bedeutet das, dass zuerst eine Konstruktorfunktion employee angelegt wirt (mit name und dept). Dann wird eine manager Konstruktorfunktion mit Eigenschaft report angelegt. Zum Schluss wird dann ein neues Objekt  employee als Prototyp für die manager  Konstruktorfunktion angelegt. Wird nun ein neues manager  Objekt erzeugt, so erbt es Eigenschaften von employee.

Hinzufügen und Löschen von Eigenschaften

In klassenbasierten Sprachen werden Klassen zur Übersetzungszeit erzeugt und zur Laufzeit instanziiert. Man kann aber weder Anzahl noch Typ von Eigenschaften einer Klasse dynamisch verändern, nach dem die Klasse erzeugt ist.

In JavaScript kann man zur Laufzeit Eigenschaften von Objekte hinzufügen oder löschen. Eine Änderung von Eigenschaften eines Objektes, das als Prototyp verwendet wurde, bewirkt dass alle Objekte, die das Objekt als Prototyp verwendet haben, geändert werden.

Unterschiede JavaScript - Java

Die u.a. Tabelle zeigt die Unterschiede zwischen JavaScript und Java. Die Punkte werden im Folgenden am Beispiel der Angestellten genauer erörtert.

 Java (klassenbasiert) JavaScript (prototypenbasiert)
Objekt, Klasse und Instanz sind unterschiedliche Konzepte alle Objekte sind auch Instanzen
Klassen werden durch Definitionen erzeugt, Instanzen werden durch Konstruktorfunktionen erzeugt Objekte werden definiert und erzeugt über Konstruktorfunktionen
Erzeugung eines neuen Objektes mit dem Operator new wie Java
Hierarchien werden als Unterklassen von Klassen bei der Klassendefinition gebildet Hierarchien werden gebildet, indem ein Objekt als Prototyp in einer Konstruktorfunktion verwendet wird
Vererbung folgt der Kette von Unterklassen Vererbung folgt der Kette der Prototypen
Klassendefinitionen beschreiben alle Eigenschaften aller Instanzen; keine dynamisches Hinzufügen oder Wegnehmen von Eigenschaften möglich Konstruktorfunktionen und Prototypen beschreiben einen Initialbestand an Eigenschaften; dynamische Anpassung ist somit möglich.

Das Angestellten Beispiel

Die folgenden Objekte und Hierarchien werden verwendet.

Erzeugen der Hierarchie

Die Realisierung der Hierarchie ist nachfolgend demonstriert. Es ist eine einfache Lösung

Objektdefinition:

Objekterzeugung:

Vererbung

Nun sehen wir im Beispiel, wie die Vererbung funktioniert und wie sich das Hinzufügen von Eigenschaften auswirkt.

Durch

  mark = new WorkerBee;

wird erreicht, dass

    name = "" und dept="general" sind (geerbt von Employee)

    projects = [] (durch Initialwert von WorkerBee)   

JavaScript erreicht dies wie folgt:

Wenn ein  new Operator erscheint, wird ein neues generisches Objekt erzeugt und an this in der korrespondierenden Konstruktorfunktion übergeben - im Beispiel die Funktion WorkerBee.Die Konstruktorfunktion setzt den Wert der Eigenschaft projects. Ebenfalls wird der Wert der (internen) Eigenschaft __proto__ auf WorkerBee.prototype (=Employee)gesetzt und somit die Prototyp-Kette bestimmt. Danach wird das so gebildete neue Objekt der Variablen mark zugewiesen.

Durch diesen Prozess werden keine Werte von gerbten Eigenschaften vorbesetzt. Wenn ein Wert einer ererbten Eigenschaft gebraucht wird, so wird er dynamische ermittelt, indem die Eigenschaft __proto__  verwendet wird und so der Prototy-Kette entlang nach der Eigenschaft gesucht wird. Wird eine solche Eigenschaft gefunden, wird ihr Wert zurück gegeben; ansonsten wird die Meldung "Objekt hat diese Eigenschaft nicht" erzeugt.

Nach der Objekterzeugung kann den Eigenschaften des Objektes mark neue Werte gegeben werden, z.B:

  mark.name= "Hacker, Mark";
  mark.dept= "admin";    
  mark.projects= ["Apache", "PHP4"];

Hinzufügen von Eigenschaften

Durch

  mark.bonus = 3000;

wird erreicht, dass nur das Objekt mark die zusätzliche Eigenschaft bonus mit Wert 3000 erhält. D.h. kein anderes Objekt besitzt dadurch die Eigenschaft bonus.

Wenn eine Eigenschaft zu einem Objekt hinzugefügt wird, das als Prototyp in einer Konstruktor Funktion verwendet wird, so besitzt jedes Objekt der Prototyp Kette diese Eigenschaft.

Durch

 Employee.prototype.speciality = "none";

wird im Beispiel folgendes bewirkt:

Spezifikation von Eigenschaften im Konstruktor

Bis jetzt waren Konstruktorfunktionen ohne Argumente. Mit Argumenten in Konstruktorfunktionen ist es möglich, den Eigenschaften (Initial-) Werte zuzuweisen.

 

Im o.a. Beispiel bekommt jim den Namen "Jones, Jim" und die Abteilung ""marketing" bei der Objektinstanziierung zugewiesen.

Die JavaScript Notation

        this.name = name || "";

verwendet den OR Operator. Dabei wird der erste Operand ausgewertet; kann er zu true ausgewertet werden, so ist das Ergebnis der erste Operand, ansonsten der zweite.

Somit wird durch die obige Anweisung der Name, der als Argument mitgegeben wird, als Initialwert verwendet; ist kein Argument beim Aufruf der Konstruktorfunktion mitgegeben, so ist der Name vorbelegt mit dem leeren String.

Die Initialisierung der Eigenschaften bezieht sich auf die lokal definierten Properties:

           jane = new Engineer("belau");

Jane’s Properties sind jetzt:

    jane.name == "";
    jane.dept == "general";
    jane.projects == []; jane.machine == "belau"   

Eine Initialisierung von ererbten Eigenschaften ist so einfach nicht möglich. Dies kann nur durch mehr Kode in der Konstruktorfunktion erreicht werden.

Spezifikation von Eigenschaften im Konstruktor (erweitert)

Bis jetzt hat eine Konstruktorfunktion immer ein generisches Objekt erzeugt und dann wurden lokale Eigenschaften hinzugefügt. Man kann auch innerhalb der Konstruktorfunktion Eigenschaften hinzufügen, indem man Konstruktorfunktionen der Prototyp-Kette aufruft:

Die Definition der Funktion Engineer:

    function Engineer (name, projs, mach) {
        this.base = WorkerBee;
        this.base(name, "engineering", projs);
        this.machine = mach || "";
    }

mit der Deklaration

    jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

wird wir folgt abgearbeitet:

  1. Der Operator new erzeugt ein generisches Objekt und setzt die Eigenschaft __proto__ auf Enginner.prototype. Damit ist WorkerBee "Vater" von dem neuen Objekt.
  2. Der new Operator reicht das neue Objekt an "this" des Engineer Konstruktors.
  3. Dieser Konstruktor erzeugt eine neue Eigenschaft mit Namen "base" und weist ihr als Wert den Konstruktor WorkerBee zu. Dadurch wird WorkerBee eine Methode des Engineer Objektes.   
    Der Name "base" ist kein Schlüsselwort von Java. Man kann beliebige Namen selbst verwenden.
  4. Der Konstruktor ruft nun die Methode "base" (=WorkerBee) auf mit den aktuellen Parametern (name, "engineering", projs). Also wird WokerBee mit den Parametern name="Doe, Jane", dept="engineering" und projs="[navigator, javascript]" aufgerufen. Da der 2. Parameter von base der fixe String "engineering" ist, haben alle Engineer-Objekte als Abteilung "engineering". Dieser Wert überschreibt den Wert von dept in Employee.
  5. Nach Rückkehr vom Aufruf des Methode base, wird noch die Eigenschaft "machine" initialisiert.
  6. Im letzten Schritt wird nun das so erzeugte Objekt an die Variable jane gebunden.

Achtung:

Dadurch daß man innerhalb des Konstruktors Enginner schreibt

this.base = WorkerBee;

ist noch nicht automatisch eine Vererbung von Eigenschaften initiiert. Eigenschaften, die danach zugefügt werden, gelten nicht mehr für das Objekt.

Beispiel: betrachten wir folgende Anweisungen:

    function Engineer (name, projs, mach) {
    this.base = WorkerBee;
    this.base(name, "engineering", projs);
    this.machine = mach || "";
  }
 

  jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

  Employee.prototype.specialty = "none";

"jane" erbt die Eigenschaft "specialty" nicht.

Im Gegensatz dazu erbt "jane" diese Eigenschaft im nachfolgenden Kodestück:

   function Engineer (name, projs, mach) {
    this.base = WorkerBee;
    this.base(name, "engineering", projs);
    this.machine = mach || "";
  }
 

  Engineer.prototype = new WorkerBee;

  jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

  Employee.prototype.specialty = "none";

Man muss den Prototyp explizite zuordnen, um die Vererbung dynamisch aufzusetzen.

Jetzt hat jane die Eigenschaft speciality ererbt und sie ist mit none vorbesetzt.

Mehr über die Vererbung von Eigenschaften

Bis jetzt haben wir gesehen, wie durch Konstruktoren und Prototypen Hierarchien aufgebaut werden können und der Vererbungsmechanismus grob funktioniert. Nun sehen wir uns Vererbung etwas genauer an.

Lokale Werte vs. geerbte Werte

Wird auf eine Eigenschaft eines Objektes zugegriffen, so führt JavaScript folgende Schritte durch:

  1. Zuerst wird geprüft, ob die Eigenschaft lokal im Objekt gefunden wird; wenn ja dann ist der Wert der Eigenschaft durch das lokale Objekt bestimmt.
  2. Wird die Eigenschaft lokal nicht gefunden, wird die Prototypkette abgearbeitet.
  3. Kann in der Prototypkette eine Eigenschaft gefunden werden, so ist der Wert dadurch bestimmt.
  4. Ist in der gesamten Prototypkette keine Eigenschaft auffindbar, so hat das Objekt die Eigenschaft nicht.

Damit ist die Art und Weise, wie man Eigenschaften definiert entscheidend dafür, ob die Vererbung so funktioniert, wie man das will. Betrachten wir folgendes Kodefragment mit Definitionen:

     function Employee () {
        this.name = "";
        this.dept = "general";
    }

    function WorkerBee () {
        this.projects = [];
    }

    WorkerBee.prototype = new Employee;

Wird nun eine Instanz von WorkerBee durch

    amy = new WorkerBee;

erzeugt, so hat die Instanz folgende Werte ihrer Eigenschaften:

    amy.name == ""
    amy.dept == "general"
    amy.projects == []

Wenn nun der Wert der Eigenschaft name im Prototyp verändert, etwa durch

        Employee.prototype.name = "Unknown";

dann wird dieser Wert nicht auf alle Instanzen von Employee propagiert. Somit gilt weiterhin

       amy.name == ""

Erklärung:

Wenn die Instanz von Employee erzeugt wird, so erhält die Instanz die lokalen Werte, also der leeren String für name, da durch WorkerBee.prototype = new Employee die Kette bestimmt ist und so name im Prototyp gefunden wird. Somit wird nicht mehr weiter gesucht und somit Employee.prototype nicht mehr untersucht.

Will man die Werte einer Objekteigenschaft zur Laufzeit ändern und dabei den neuen Wert an alle "Nachfahren" des Objektes vererben, so darf man die Eigenschaft nicht in der Konstruktorfunktion definieren, sondern in dem zugehörenden Prototyp.

Angewandt auf das obige Beispiel bedeutet dies:

    function Employee () {
        this.dept = "general";
    }

    Employee.prototype.name = "";

    function WorkerBee () {
        this.projects = [];
    }

    WorkerBee.prototype = new Employee;

    amy = new WorkerBee;

    Employee.prototype.name = "Unknown"

Jetzt gilt:

    amy.name == "Unknown"

Schlussfolgerung:

Wenn Initialwerte für Objekteigenschaften zur Laufzeit verändert und vererbt werden sollen, muss man die Eigenschaft im Prototyp des Konstruktor definieren, nicht innerhalb der Konstruktorfunktion selbst.

Objekte der Prototypkette

In klassischen objektorientierten Programmiersprachen existiert meist ein Operator, etwa instanceof in Java, um abzufragen, ob ein Objekt in der Prototypkette enthalten ist. Solch ein Operator existiert in JavaScript nicht; man kann aber recht einfach eine Funktion entwickeln, die die gewünschte Funktionalität bereit stellt.

Wird der Operator new mit einer Konstruktorfunktion verwendet (WorkerBee.prototype = new Employee;), so setzt JavaScript die Eigenschaft __proto__ des neu erzeugten Objektes auf den Wert, der durch die prototyp Eigenschaft der Konstruktorfunktion bestimmt ist. Damit kann man dann die Kette testen.

In unserem Beispiel wird durch

    chris = new Engineer("Test, Chris", ["Communicator"], "hp01");

ein Objekt erzeugt, für das gilt:

    chris.__proto__ == Engineer.prototype
    chris.__proto__.__proto__ == WorkerBee.prototype
    chris.__proto__.__proto__.__proto__ == Employee.prototype
    chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype
    chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null

Damit kann man eine JavaScript Funktion instanceOf() schreiben:

    function instanceOf(object, constructor) {
        while (object != null) {
            if (object == constructor.prototype)
                return true;
            object = object.__proto__;
        }
        return false;
    }
 

Damit gilt für unser Beispiel:

    instanceOf (chris, Engineer)     == true

    instanceOf (chris, WorkerBee)    == true

    instanceOf (chris, Employee)     == true

    instanceOf (chris, Object)       == true

    instanceOf (chris, SalesPerson)  == false

Keine Mehrfachvererbung

Einige objektorientierte Programmiersprachen, wie C++, erlauben Mehrfachvererbung, d.h. ein Objekt kann Eigenschaften von mehreren unabhängigen Eltern erben.

JavaScript erlaubt Mehrfachvererbung nicht. Die Vererbung von Eigenschaften wird zur Laufzeit immer durch Suche in der Prototypkette umgesetzt. Da ein Objekt einen einzigen Prototypen zugeordnet hat, sind Mehrfachvererbungen nicht möglich.

Globale Information in Konstruktoren

Wenn globale Variable in Konstruktorfunktionen verwendet werden, können oft nicht erwünschte Effekte eintreten.

Nehmen wir an, in unserem Beispiel sollen alle Angestellten eine Personalnummer bekommen und diese Nummer soll fortlaufend und ohne Lücken sein.

Die erste Idee ist:

Beispiel 5.2-1

Auf den ersten Blick funktioniert dies.

Aber bei dem vollständigen Beispiel, also wenn Objekte über die Objekthierarchie (mit der base Eigenschaft) gebildet werden, entstehen Lücken, da das Inkrementieren bei jedem Aufruf des Employee Konstruktors ausgeführt wird.

    var idCounter = 1;

    function Employee (name, dept) {
        this.name = name || "";
        this.dept = dept || "general";
        this.id = idCounter++;
    }

    function Manager (...
    Manager.prototype = new Employee;  /* idCounter == 2 */

    function WorkerBee (... this.base = Employee; this.base(name,dept); ...
    WorkerBee.prototype = new Employee;  /* idCounter == 3 */

    function Engineer (...
    Engineer.prototype = new WorkerBee;   /* idCounter == 4 wg. base */

    function SalesPerson (...
    SalesPerson .prototype = new Employee; /* idCounter == 5 */

    mark = new Egnineer("Mark"); /* mark.id == 5, idCounter == 6 */

Je nach Aufgabenstellung muss eine Lösung gesucht werden. In unserem Beispiel könnte sie etwa wie folgt aussehen: nur wenn employee mit einem Argument (name ungleich leerer String) aufgerufen wird, wird idCounter inkrementiert.

    function Employee (name, dept) {
        this.name = name || "";
        this.dept = dept || "general";

        if (name)
            this.id = idCounter++;
    }
 


Übung 5.1-1

Verdeutlichen Sie sich den Vererbungsmechanismus, in dem Sie das Angestelltenbeispiel in ein lauffähiges Programm bringen. Dabei sollen von jedem Angestelltentyp mindestens eine Person mit den entsprechenden Daten eingegeben werden können. Die Objekthierarchie ist auszugeben.