XT Knowledge Base
Hauptseite | Über | Hilfe | FAQ | Spezialseiten | Anmelden

Druckversion | Impressum | Datenschutz | Aktuelle Version

OOP mit JavaScript

(Unterschied zwischen Versionen)

(Überladen von Methoden)
K (Überladen von Methoden)
Zeile 658: Zeile 658:
I am a circle with red color and I have two parents:
I am a circle with red color and I have two parents:
I am a shape
I am a shape
-
I am a colored object!
+
I am a colored object
</pre>
</pre>

Version vom 13:10, 8. Sep. 2010

Inhaltsverzeichnis

Einleitung

JavaScript unterscheidet sich wesentlich von Sprachen wie C++ oder auch PHP. Gerade der objektorientierte Ansatz von JavaSrript baut auf Prototypen auf. D.h. die grundlegende Denkweise ist völlig unterschiedlich. In vielen Tutorials wird versucht, JavaScript in das Schema der bekannten Programmiersprachen zu pressen, was durchaus sinnvoll sein kann, um bestehende Konzepte umzusetzen. Allerdings ermöglicht JavaScript völlig neue Konzepte und Möglichkeiten, die es in C++, Java und ähnlichen Sprachen nicht gibt. Beispielsweise kann sich das Verhalten einer Klasse während der Laufzeit ändern. Tatsächlich werden die Klassen sogar erst während der Laufzeit erzeugt.

Dennoch ist es sinnvoll, sich JavaScript aus dem Blickwinkel der bekannten Konzepte zu betrachten, wobei man im Auge behalten muss, wobei diese nicht vollständig umgesetzt werden können. Allerdings wird mit den bekannten Konzepten nur ein Teil der Möglichkeiten ausgeschöpft, die JavaScript bietet.

Klassen

Folgende Klasse repräsentiert einen Punkt im zweidimensionalen Raum.

function Point2D( x, y )
{
  this.x = x ? x : 0;
  this.y = y ? y : 0;
}
Der Ausdruck
<argument> ? <argument> : 0;
sorgt dafür, dass die Variablen auch dann richtig initalisiert werden, wenn keine Argumente übergeben werden. Werden alle übergebenen Parameter auf diese Weise überprüft und mit default-Werten versehen kann man einen Default-Konstruktor realisieren, der auf jeden Fall vorhanden sein sollte.

Eine JavaScript-Klasse sieht aus wie eine gewöhnliche Funktion. Diese Funktion übernimmt aber gleichzeitig die Aufgabe einer Klassen-Deklaration und die eines Konstruktors.

Die Erzeugung einer Instanz und der Zugriff darauf geschieht wie folgt:

  var p = new Point2D( 10, 15 );
  alert( "x: " + p.x + ", y: " + p.y ); // x: 10, y: 15

Im Vergleich zur Deklaration erscheint die Instanziierung dann aber eher so, wie man es erwarten würde.

Auch folgendes ist nun möglich:

  var p = new Point3D;
  p.x = 10;
  p.y = 15;
  alert( "x: " + p.x + ", y: " + p.y ); // x: 10, y: 15

Methoden

Insgesamt werden drei Möglichkeiten genannt, einer Klasse Methoden hinzuzufügen:

Zuweisung von Funktionen im Konstruktor

function Point2D( x, y )
{
  this.x = 0;
  this.y = 0;

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

  this.set( x ? x : 0, y ? y : 0 );
}

In diesem Fall wird im Konstruktor eine set()-Methode gesetzt und direkt aufgerufen. Letztendlich ist das nichts anderes als die Initialisierung einer Funktionsvariablen.

Diese Funktion ist nun auch "von außen" aufrufbar:

  var p = new Point2D( 1, 3 );
  p.set( 3, 1 );
  alert( "x: " + p.x + ", y: " + p.y ); // x: 3, y: 1

Deklaration von Methoden über "prototype"

Eine etwas übersichtlichere und empfohlene Möglichkeit ist die nachträgliche Zuweisung von Methoden:

function Point2D( x, y )
{
  this.x = 0; 
  this.y = 0;         
  this.set( x, y );
}

Point2D.prototype.set = function( x, y )
{
  this.x = x ? x : 0;
  this.y = y ? y : 0;
}

var p2 = new Point2D;
p2.set( 30, 21 );

In diesem Beispiel wird eine function "set" dem prototype der Point2D-Klasse hinzugefügt und kann auch im Konstruktor aufgerufen werden. Mit Ausnahme der Tatsache, dass prototype-Funktionen nicht auf private Member zugreifen können, gibt es keinen Unterschied zu den Funktionen, die direkt im Konstruktor erzeugt werden.

Private Member und Methoden

Private Member werden wie lokale Variablen deklariert. Der Zugriff kann nur über Methoden erfolgen, die direkt im Konstruktor zugewiesen werden:

function Point2D( x, y )
{
  var m_x = x ? x : 0;
  var m_y = y ? y : 0;

  this.getX = function(){ return m_x; }
  this.setX = function( x ){ m_x = x; }
  this.getY = function(){ return m_y; }
  this.setY = function( y ){ m_y = y; }
}

Die Klasse Point2D enthält zwei private Membervariablen, auf die nur mit den get/set-Methoden zugegriffen werden kann.

Wie man sehen kann, wird auf diese (eigentlich lokalen) Variablen nicht über this zugegriffen. Das ist auch der Grund, warum andere Methoden, die nicht im Konstruktor erzeugt wurden, keinen Zugriff auf diese privaten Member haben:

Point2D.prototype.toString = function()
{
  return "x: " + m_x + ", y: " + m_y;                 // schlägt fehl (m_x: undefined, m_y: undefined)
  return "x: " + this.m_x + ", y: " + this.m_y;       // schlägt fehl (this.m_x: undefined und this.m_y: undefined)
  return "x: " + this.getX() + ", y: " + this.getY(); // so funktioniert es
}

Private Methoden werden wie lokale Funktionen definiert:

    function Point2D( x, y )
    {
      var m_x;
      var m_y;
      
      function init( x, y )
      {
        m_x = x ? x : 0;
        m_y = y ? y : 0;
      }
      
      init( x, y );
    
      this.getX = function(){ return m_x; }
      this.setX = function( x ){ m_x = x; }
      this.getY = function(){ return m_y; }
      this.setY = function( y ){ m_y = y; }
      this.setXY = function( x, y ){ init( x, y ); }
    }

Die Funktion init() ist nur innerhalb des Konstruktors sichtbar, kann aber von Funktionen aufgerufen werden, die innerhalb des Konstruktors angelegt wurden. Die Funktion this.setXY() nutzt nun die private Funktion init(), um beide Variablen auf einmal zu initialisieren.

    var p2 = new Point2D( 1, 2 );
    log( "x: " + p2.getX() + ", y: " + p2.getY() );
        
    p2.setXY( 3, 4 );
    log( "x: " + p2.getX() + ", y: " + p2.getY() );

Vererbung

Mit JavaScript ist es möglich, alle Methoden und Attribute einer Klasse an den prototype einer anderen Klasse zu übergeben:

Point3D.prototype = new Point2D();

Ohne weiteres Zutun hat man nun zweite Klasse, die eine exakte Kopie der ersten darstellt. Nun kann der prototype der zweiten Klasse beliebig verändert werden, ohne Auswirkungen auf die erste Klasse zu haben:

function Point2D( x, y )
{
  this.x = 0; 
  this.y = 0;         
  this.set( x, y );
}

Point2D.prototype.set = function( x, y )
{
  this.x = x ? x : 0;
  this.y = y ? y : 0;
}

Point3D.prototype = new Point2D();

function Point3D( x, y, z )
{
  this.z = z ? z : 0;
  this.set( x, y );
}

var p = new Point3D( 1, 2, 3 );
alert( "x: " + p.x + ", y: " + p.y + ", z: " + p.z ); // x: 1, y: 2, z: 3

Natürlich könnte nun Point3D eine eigene set()-Methode anbieten, die x, y und z als Parameter erhält:

Point3D.prototype.set = function( x, y, z )
{
  this.x = x ? x : 0;
  this.y = y ? y : 0;
  this.z = z ? z : 0;  
}

Konstruktoren

Schöner wäre es allerdings, wenn man den Konstruktor der Klasse aufrufen könnte, dessen Member und Methoden man geerbt hat.

Das ist tatsächlich einfach zu realisieren:

Point3D.prototype.set = function( x, y, z )
{
  Point2D.call( this, x, y );
  this.z = z ? z : 0;  
}

Hier wird diel call()-Methode von Funktions-Objekten aufgerufen und ein Zeiger auf this samt Parameterliste übergeben. this zeigt nun auf ein Point3D-Objekt und wird dem Point2D-Konstruktor untergeschoben. Dieser initialisiert nun den Teil, der an die Point3D-Klasse vererbt wurde.

Es ist empfehlenswert, bei allen übergebenen Parameter davon auszugehen, dass sie auch "undefined" sein kann. Das ist vor allem dann der Fall, wenn die Klasse vererbt wird. (Point3D.prototype = new Point2D);

Virtuelle Methoden

Methoden sind immer virtuell. Anders als zum Beispiel bei C++ können virtuelle Funktionen auch im Konstruktor aufgerufen werden, wobei tatsächlich die Funktion der abgeleiteten Klasse ausgeführt wird. Auch wenn dies nicht immer wünschenswert ist.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8">
    <title>OOP mit JavaScript</title>
    <script type="text/javascript">

    function log( str )
    {
        document.f.t.value += str + "\n";
    }
   
    function Point2D( x, y )
    {
        this.x = x ? x : 0;
        this.y = y ? y : 0;
        if( x )
            log( this.getAsString() );
    }
    
    Point2D.prototype.getAsString = function()
    {
        return this.toString();
    }
    
    Point2D.prototype.toString = function()
    {
        return "x: " + this.x + ", y: " + this.y;
    }
    
    Point3D.prototype = new Point2D;

    function Point3D( x, y, z )
    {
        this.z = z ? z : 0;
        Point2D.call( this, x, y );
    }

    Point3D.prototype.toString = function()
    {
        return Point2D.prototype.toString.call( this ) + ", z: " + this.z;
    }
    
    Point4D.prototype = new Point3D;

    function Point4D( x, y, z, q )
    {
        this.q = q ? q : 0;
        Point3D.call( this, x, y, z );
    }

    Point4D.prototype.toString = function()
    {
        return Point3D.prototype.toString.call( this ) + ", q: " + this.q;
    }
    
    function init()
    {
        var p2 = new Point2D( 2, 3 );
        var p3 = new Point3D( 2, 3, 5 );
        var p4 = new Point4D( 2, 3, 5, 6 );
    }    

    </script>
</head>
<body onLoad="init();">
    <form name="f" class="debug">
    <textarea name="t" cols=80 rows=15></textarea>
    </form>
</body>
</html>

In diesem Beispiel werden drei Klassen implementiert: Point2D, Point3D und Point4D. Point3D erbt von Point2D x und y und Point4D erbt x und y von Point2D indirekt über Point3D und zusätzlich noch z von Point3D.

Im Konstruktor wird jeweils der Vorgänger-Konstruktor explizit aufgerufen, um die geerbten Member zu initialisieren.

Jede Klasse erhält nun eine Methode toString(), die aus den jeweiligen Koordinaten einen String erzeugt und zurück liefert. Auch hier wird Gebrauch von der jeweils geerbten Implementierung gemacht.

Im Konstruktor von Point2D wird nun getestet, wie es sich mit der Virtualität verhält: Wenn der Konstruktor nicht als Default-Konstruktor aufgerufen wird (was zum Beispiel über die Anweisung
Point4D.prototype = new Point3D;
geschieht), soll toString() des aktuellen Objekts aufgerufen und das Ergebnis im Textfeld angezeigt werden.

Der Reihe nach werden nun Objekte aller Klassen angelegt und damit jeweils eine Ausgabe ausgelöst:

x: 2, y: 3
x: 2, y: 3, z: 5
x: 2, y: 3, z: 5, q: 6

In diesem Beispiel wurden die eigenen Membervariablen initialisiert, bevor der Vorgänger-Konstruktor aufgerufen wurde, was nicht unbedingt üblich ist. Würde man die Reihenfolge ändern, erhielte man folgende Ausgabe, was gleichzeitig auf das Problem mit virtuellen Methodenaufrufen aus dem Konstruktor heraus hinweist:

x: 2, y: 3
x: 2, y: 3, z: undefined
x: 2, y: 3, z: 0, q: undefined

Beim Aufruf von toString() aus dem Konstruktor der Basisklasse sind also noch nicht alle Member-Variablen initialisiert. Aus gutem Grund ist das also in C++ nicht möglich.

Nicht-virtuelle Methoden

Folgendes Beispiel verdeutlicht, dass es in JavaScript keine nicht-virtuellen Methoden gibt:

    function log( str )
    {
        document.f.t.value += str + "\n";
    }
    
    function Shape()
    {
        this.showMe = function()
        {
            log( "I am a shape!" );
        }
        
        this.showMe();
    }
    
    Box.prototype = new Shape;
    
    function Box()
    {
        this.showMe = function()
        {
            log( "I am a box!" );
        }
        
        Shape.call( this );        
    }
    
    Circle.prototype = new Shape;
    
    function Circle()
    {
        this.showMe = function()
        {
            log( "I am a circle!" );
        }
        
        Shape.call( this );
    }
    
    function main()
    {
        var s = new Shape;
        var b = new Box;
        var c = new Circle;
    }

Dieses Beispiel ist ähnlich dem mit den PointxD-Klassen. Box und Circle rufen jeweils den Shape-Konstruktor auf, der dann wiederum mit showMe() eine Ausgabe im Textfeld erzeugt. Tatsächlich erscheint in diesem Beispiel drei Mal die Ausgabe "I am a shape!".

Allerdings taucht ein unangenehmes Problem auf, sobald ich in der Funktion main einen weiteren Aufruf hinzufüge:

    function main()
    {
        var s = new Shape;
        var b = new Box;
        var c = new Circle;
        c.showMe();
    }

Die Ausgabe entspricht nicht dem, was beabsichtigt war:

I am a shape!
I am a shape!
I am a shape!
I am a shape!

Tatsächlich wird durch den Aufruf der Vorgängermethode die Variable this.showMe ersetzt.

Würde nun zuerst der Shape-Konstruktor aufgerufen und dann die this.showMe-Funktion der abgeleiteten Klasse ersetzt, hätte das zwar den erwarteten Effekt, aber anschließend wäre die this.showMe-Methode des Shapes in Box und Circle-Klassen einfach nicht mehr aufrufbar.

Statische Methoden und Variablen

Die statischen Methoden einer JavaScript-Klasse werden nicht dem prototype hinzugefügt, sondern der Klasse selbst:

function Static()
{
}

Static.staticVar = 0;
Static.staticMethod = function()
{
  Static.staticVar++;
  alert( "Static.staticVar = " + Static.staticVar );
}

Static.staticMethod();
Static.staticMethod();

Statische Methoden können aufgerufen werden, auch wenn keine Instanz der Klasse existiert. Sie werden über den Klassennamen referenziert.

Eine statische Variable existiert nur ein einziges Mal, unabhängig davon, wieviele Instanzen dieser Klasse erzeugt wurden. Auf die statischen Variablen können alle Instanzen zugreifen.

Statische Variablen können zum Beispiel verwendet werden, um die Anzahl der Instanzen einer Klasse zu zählen. Dabei muss allerdings beachtet werden, dass bei der Vererbung einer Klasse ebenfalls eine Instanz erzeugt wird.

Mehrfach-Vererbung

Mit einem kleinen Trick ist auch Multiple Inheritance möglich, auch wenn es in vielen JavaScript-Tutorials anderslautende Meinungen gibt. Um diesen Trick zu verstehen, muss man nur einen Moment über folgenden Ausdruck meditieren:

NewClass.prototype = new OldClass;

Was passiert hier?

NewClass.prototype ist ein Array und der Ergebnis von new OldClass ist ebenfalls ein Array. Folglich wird einfach nur ein Array in ein anderes kopiert.

Warum wird nicht OldClass.prototype kopiert?

Wird der Konstruktor einer Klasse nicht aufgerufen, enthält das Objekt nur alle im Code explizit dem prototype zugewiesenen Funktionen, man würde also nur das Interface kopieren, was unter Umständen durchaus sinnvoll sein könnte.

Nach dem Aufruf des Konstruktors werden implizit über this auch alle Member und im Konstruktor erzeugten Methoden dem prototype zugewiesen.

So kann man sich den Inhalt von prototype jeder Klasse anschauen.

for( m in AnyClass.prototype )
{
  log( m + ": " + AnyClass.prototype[m] );
}

Erzeugt man nun eine Instanz einer solchen Klasse, kann man sich dessen Inhalt ebenfalls auf diese Weise betrachten:

var anyInstance = new AnyClass;
for( m in anyInstance )
{
  log( m + ": " + anyInstance[m] );
}

Aufgrund dieser Umstände kann man nun die Funktionsweise leicht durchschauen.

Für Mehrfachvererbung müsste es also nun gelingen, den Inhalt mehrerer Klassen in den prototype der neuen Klasse zu übernehmen.

Leider funktioniert NewClass.prototype.concat( new OldClass ) nicht in der gewünschten Weise, da die Methode concat() für prototype nicht existiert.

Daher kann man sich mit einer neuen Methode in der impliziten Basisklasse Object behelfen:

    Object.prototype.inherits = function( parent ) 
    {
        var obj = new parent;
        for( m in obj )
        {
            this.prototype[m] = obj[m];
        }
    };

Hier wird eine Instanz der parent-Klasse erzeugt und die Inhalte in den prototype übernommen. So lassen sich beliebig viele Klassen erben, wobei die üblichen Probleme mit den Namenskonflikten gleicher Member und Methoden bestehen. In diesem Fall gewinnt immer die zuletzt hinzugefügte Klasse, die alle Member, die mit gleichen Namen bereits existieren überschreibt.

Beispiel:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" >
<head>
    <meta http-equiv="content-type" content="text/html;charset=UTF-8">
    <title>OOP mit JavaScript</title>
    <script type="text/javascript" src="oop.js"></script>
    <script type="text/javascript">

    function log( str )
    {
        document.f.t.value += str + "\n";
    }
    
    Object.prototype.inherits = function( parent ) 
    {
        var obj = new parent;
        for( m in obj )
        {
            this.prototype[m] = obj[m];
        }
    };
    
    function Motor()
    {
        this.max_speed = 100;   
    }
    
    Motor.prototype.antreiben = function()
    {
        log( "antreiben" );
    }
    
    function Getriebe()
    {
        this.max_gear = 4;
    }
    
    Getriebe.prototype.schalten = function()
    {
        log( "schalten" );
    }
    
    Auto.inherits( Motor );
    Auto.inherits( Getriebe );
    
    function Auto()
    {
    }
    
    Auto.prototype.fahren = function()
    {
        log( "fahren" );
        this.antreiben();
        this.schalten();
    }
    
    function main()
    {
        var auto = new Auto;
        auto.fahren();
        
        log( "\nAuto.prototype:\n");
        for( m in Auto.prototype )
        {
            log( m + ": " + Auto.prototype[m] );
        }
    }
    </script>
</head>
<body onLoad="main();">
    <form name="f" class="debug">
    <textarea name="t" cols=80 rows=15></textarea>
    </form>
</body>
</html>

Es werden zwei Klassen "Motor" und "Getriebe" deklariert. Beide Klassen sollen in die Klasse "Auto" übernommen werden. Mit den Anweisungen

    Auto.inherits( Motor );
    Auto.inherits( Getriebe );

wird nun die neue Object-Methode inherits() aufgerufen, die den Auto.prototype jeweils um die Fähigkeiten der geerbten Klassen ergänzt. Damit stehen die Methoden antreiben() und fahren() nun auch dem Auto zur Verfügung.

Überladen von Methoden

Oft möchte man eine geerbte Methode einfach nur um Details erweitern. Dazu reicht es oft, wenn man die Vorgänger-Methode aufruft und dann seinen eigenen Code ausführt oder das in umgekehrter Reihenfolge tut oder auch eigenen Code ausführt, die Vorgänger-Methode aufruft und dann weiteren Code ausführt.

Das ist prinzipiell möglich, in dem man folgendes schreibt:

<parent class>.prototype.<method>.call( this [, parameter list] );

Tatsächlich lässt sich so jede Methode jeder beliebigen Klasse direkt aufrufen, ganz egal, ob man diese nun geerbt hat oder nicht.

Etwas eleganter wäre es, wenn man schreiben könnte:

this.getParent().<method>([parameter list]);

Das hätte den Vorteil, dass man den tatsächlichen parent erwischt und auch der Aufruf ist etwas intuitiver und auch kürzer.

Bei einer möglichen Mehrfachvererbung ergibt sich dann wieder das Problem, dass man die parents auseinanderhalten muss, was dann eher wieder für den ersten Ansatz spricht. Allerdings gibt es auch die Möglichkeit, die weiter oben vorgeschlagene inherits()-Methode entsprechend zu erweitern:

    Object.prototype.inherits = function( parent ) 
    {
        var obj = new parent;
        for( m in obj )
        {
            this.prototype[m] = obj[m];
        }
        
        if( !this.prototype.parent )
            this.prototype.parent = new Array();
        this.prototype.parent.push( obj );
    };

    Object.prototype.getParent = function( idx )
    {
        return this.parent[idx?idx:0];
    }
    
    Object.prototype.getParentCount = function()
    {
        return this.parent.length;
    }

Hier wird ein Array erzeugt, das eine Liste aller Parent-Klassen aufnehmen kann. Mit getParent() kann nun auf die Elternklasse zugegriffen werden, wobei defaultmäßig immer der erste Parent zurückgeliefert wird. So wird der häufige Fall der einfachen Vererbung ideal unterstützt.

Bei Mehrfach-Vererbung liefert getParentCount() nun die Anzahl der (direkt) geerbten Klassen zurück und getParent() kann mit einem Index aufgerufen werden, der dann die entsprechende Elternklasse liefert. Die Reihenfolge der Parents stimmt mit der überein, in der sie geerbt wurden.

Eine Beispiel-Anwendung:

    function log( str )
    {
        document.f.t.value += str + "\n";
    }
    
    function ColoredObject( color )
    {
        this.color = color ? color : 0;

        this.showMe = function()
        {
            log( "I am a colored object" );
        }
    }
    
    function Shape()
    {
        this.showMe = function()
        {
            log( "I am a shape" );
        }
    }
    
    Box.inherits( Shape );
    
    function Box()
    {
        this.showMe = function()
        {
            log( "I am a box and my parent says:" );
            this.getParent().showMe();
        }
    }
    
    Circle.inherits( Shape );
    Circle.inherits( ColoredObject );
    
    function Circle()
    {
        ColoredObject.call( this, "red" );
        this.showMe = function()
        {
            log( "I am a circle with " + this.color + " color and I have two parents:" );
            
            for( var i = 0; i < this.getParentCount(); i++ )
                this.getParent(i).showMe();
        }
    }
    
    function main()
    {
        var s = new Shape;
        var b = new Box;
        var c = new Circle;
        
        s.showMe();
        b.showMe();
        c.showMe();
    }

Box wird nur von Shape abgeleitet, während Circle die Klassen Shape und ColoredObject erbt.

Die Funktionen showMe() zeigen nun an, wer wer ist und von wem er abgeleitet wurde:

I am a shape
I am a box and my parent says:
I am a shape
I am a circle with red color and I have two parents:
I am a shape
I am a colored object

Quellen

Dieses Tutorial basiert auf einen Artikel von Gavin Kistner und dessen Korrektur durch Shelby H. Moore III.

ECMAScript Language Specification

Object-oriented JavaScript for the C++ programmer

An example of using true inheritence with plain JavaScript

SELFHTML - JavaScript/DOM