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

Druckversion | Impressum | Datenschutz | Aktuelle Version

OOP mit JavaScript

Version vom 07:58, 8. Sep. 2010 bei Monettenom (Diskussion | Beiträge)

Inhaltsverzeichnis

Einleitung

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

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" src="oop.js"></script>
    <script type="text/javascript">

    function log( str )
    {
        document.f.t.value += str + "\n";
    }

    Function.prototype.extends = function( parent )
    {
        this.prototype = new parent;
        this.prototype.constructor = this;
        this.prototype.parent = parent;
    }   
   
    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.extends( 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.extends( 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. Um sich etwas Arbeit zu sparen, habe ich die Funktion extends() implementiert, die die Initialisierung von prototype für die Vererbung übernimmt.

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.extends( 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.


Finden

Blättern
Hauptseite
XT Knowledge Base-Portal
Aktuelle Ereignisse
Letzte Änderungen
Zufällige Seite
Konfiguration
Hilfe
Ändern
Quelltext betrachten
Bearbeitungshilfe
Seitenoptionen
Diskussion
Neuer Abschnitt
Druckversion
Seitendaten
Versionen
Links auf diese Seite
Änderungen an verlinkten Seiten
Meine Seiten
Anmelden
Spezialseiten
Neue Seiten
Dateiliste
Statistik
Mehr …