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

Druckversion | Impressum | Datenschutz | Aktuelle Version

OOP mit JavaScript

(Unterschied zwischen Versionen)

(Virtuelle Methoden)
K (Virtuelle Methoden)
Zeile 173: Zeile 173:
     <meta http-equiv="content-type" content="text/html;charset=UTF-8">
     <meta http-equiv="content-type" content="text/html;charset=UTF-8">
     <title>OOP mit JavaScript</title>
     <title>OOP mit JavaScript</title>
-
    <script type="text/javascript" src="oop.js"></script>
 
     <script type="text/javascript">
     <script type="text/javascript">

Version vom 08:14, 8. Sep. 2010

Inhaltsverzeichnis

Einleitung

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

Eine Gegenüberstellung von OOP mit C++ und JavaScript gibt es hier: Object-oriented JavaScript for the C++ programmer

Deklaration von 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. (Default-Konstruktor).

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.

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. Tatsächlich scheint es keinen Unterschied zu machen, welche Methode gewählt wird.

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
}

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.

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.