Thema dieser Seite

Hier beschreibe ich meine Erfahrungen mit dem objektorientierten Programmieren in JavaScript mit functions. Zur Erstellung dieser Seite haben mir folgende Links geholfen:

Allgemeine Tipps zum Programmieren mit JavaScript findet man auf dieser Seite. Um objektorientiert zu programmieren, ohne die function-Syntax zu verwenden, habe ich die Seite Objektorientiertes Programmieren in JavaScript mit classes erstellt.

Beispiel-Anwendung

Die folgende Beispiel-Anwendung ist in objektorientiertem JavaScript mit Konstruktor-Funktionen geschrieben. Der Code, der in den folgenden Abschnitten besprochen wird, stammt aus dieser Anwendung.

Die Anwendung selbst zeichnet bunte Objekte, wenn man auf den schwarzen Bereich klickt.

Dein Browser kann dieses Element leider nicht anzeigen!

Diese Anwendung verwendet folgende JavaScript-Dateien:

Klassendefinition

Eine Klasse in JavaScript wird bei dieser Herangehensweise eigentlich eine Konstruktor-Funktion realisiert. Im folgenden Beispiel wird die Klasse DrawingObject mit zwei öffentlichen Variablen und einer öffentlichen Methode definiert. Die Klasse hat einen Konstruktor mit zwei Parametern, die dann in den öffentlichen Variablen gespeichert werden.

      function DrawingObject(xCenter, yCenter) {

        this.mXCenter = parseInt(xCenter);
        this.mYCenter = parseInt(yCenter);
      
        //dummy draw method
        this.draw = function(canvasDrawer) {
          console.log("The draw method has not been overwritten");
        }
      }
    

Will man öffentlich sichtbare Variablen oder Functions außerhalb der Klasse verwenden, so kann man ein Objekt der Klasse erstellen und darüber dann zugreifen. Im folgenden ein Beispiel:

      let drawingObject = new DrawingObject(2,3);
      alert(drawingObject.mXCenter + ", " + drawingObject.mYCenter);
      drawingObject.draw();
    

Hier passieren folgende Dinge:

Meine Empfehlungen für Klassen:

Durch die Verwendung von Klassen ergeben sich folgende Vorteile:

Private Variablen und Konstanten

Variablen und Konstanten, die nur in der Klasse sichtbar sind, können normal mit var und const angelegt werden. Sie können dann auch normal in der Klasse aufgerufen werden.

Ich habe mir angewöhnt Member-Variablen von Klassen immer mit einem m beginnen zu lassen, damit man überall erkennt, dass es sich nicht um lokale Variablen handelt.

Konstanten verwende ich lediglich auf Klassen-Ebene und man erkennt sie immer am Namen, der nur Großbuchstaben und Unterstriche beinhält. Daher muss ich nicht zwischen Konstanten auf Klassenebene und Blockebene unterscheiden, bzw. dies auch nicht kennzeichnen.

      function SampleApplication() {

        var mCanvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400);
        var mDrawingObjects = [];
        var mCurrentDrawingObjectId = 0;
        const MAX_DRAWING_OBJECTS = 20;

        ...

        function addDrawingObject(drawingObject) {
          mCurrentDrawingObjectId++;
          if (mDrawingObjects.length < MAX_DRAWING_OBJECTS) {
            mDrawingObjects.push(drawingObject);
          }
          else {
            mCurrentDrawingObjectId = mCurrentDrawingObjectId%MAX_DRAWING_OBJECTS;
            mDrawingObjects[mCurrentDrawingObjectId] = drawingObject;
          }
        }

        ...
      }
      
    

Werden die privaten Variablen außerhalb der Klasse aufgerufen, so wird der Wert undefined angezeigt.

      let anotherSampleApplication = new SampleApplication();
      alert(anotherSampleApplication.mCurrentDrawingObjectId); // undefined
      alert(anotherSampleApplication.MAX_DRAWING_OBJECTS); // undefined
    

Das hat den Vorteil, dass Variablen, die nur für die interne Logik in Klassen benötigt werden, außerhalb der Klasse nicht sichtbar und auch nicht änderbar sind.

Öffentlich sichtbare Variablen

Man kann mit dem Kenner this. öffentlich sichtbare und veränderbare Variablen (Eigenschaften) definieren. (Dies ist ähnlich wie die Verwendung von öffentlich sichtbaren Functions.) this weist in diesem Fall auf das Objekt hin, dem diese Eigenschaft gehört.

      function DrawingObject(xCenter, yCenter) {

        this.mXCenter = parseInt(xCenter);
        this.mYCenter = parseInt(yCenter);
      
        ...
      }
    

Allerdings habe ich mir angewöhnt solche Variablen nur mit get-Methoden auszugeben bzw. nur mit set-Methoden zu verändern. Somit habe ich die Kontrolle, ob die Variable von außen gelesen bzw. verändert werden kann. Und wenn dies der Fall ist, kann ich noch Zusicherungen machen (z.B. auf undefined abprüfen), bevor mit dieser Variable gearbeitet wird.

So macht es auch nicht so viel aus, dass ich bis jetzt noch keinen Weg gefunden habe, um Konstanten öffentlich sichtbar zu machen.

Allerdings benötigt man diese öffentliche Variablen in meinem Beispiel bei der Vererbung.

Öffentlich sichtbare Functions ("public methods")

Innerhalb der Klasse können öffentlich sichtbare Functions definiert werden. (Im Vergleich zu Java wären dies die Methoden, die mit dem Kenner public gekennzeichnet sind.)

Im Gegensatz zur normalen Deklaration der Functions mit

      function <Function-Name>(<Parameter1>, <Parameter2>, ...) {}
    

wird hier die Definition folgendermaßen durchgeführt:

      this.<Function-Name> = function(<Parameter1>, <Parameter2>, ...) {}
    

Dabei empfiehlt es sich die Function-Namen mit Kleinbuchstaben beginnen zu lassen. Der Kenner this weist dabei wieder auf das Objekt hin, dem die öffentliche Funktion gehört. Im folgenden Code sieht man nun zwei Beispiele für öffentliche Methoden.

      function CanvasDrawer(canvasDivId, canvasId, height) {

        var mCanvasDivId = canvasDivId;
        var mCanvasId = canvasId;
        var mHeight = height;
        var mWidth = $("#" + canvasDivId).width();
        var mCanvas = $("#" + canvasId)[0];
        mCanvas.width = mWidth;
        mCanvas.height = mHeight;
        var mContext = mCanvas.getContext("2d");
        var mFontHeight = 30;
        mContext.font = mFontHeight + "px Arial";
      
      
        ////////////////////////////////
        // PUBLIC METHODS             //
        ////////////////////////////////
        this.clearCanvas = function(color = 'rgb(0, 0, 0)') {
          this.setColor(color);
          mContext.fillRect(0, 0, mWidth, mHeight);
        }
      
        this.setColor = function(color) {
          mContext.fillStyle = color;
        }
        ...
      }
    

Mit clearCanvas und mit setColor werden zwei öffentliche Methoden definiert. Im Code von clearCanvas sieht man sogar, dass die andere öffentliche Methode der Klasse aufgerufen wird. Geschieht so ein Methoden-Aufruf innerhalb der Klasse, so muss man dies mit this.<Function-Name>() tun.

Will man eine öffentliche Methode außerhalb der Klasse aufrufen, so muss man zunächst ein Objekt der Klasse erstellen und dann die Methode des Objekts ausführen.

      function SampleApplication() {

        var mCanvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400);
        ...
       
        function drawBackground() {
          mCanvasDrawer.clearCanvas();
          mCanvasDrawer.setColor("rgb(200, 200, 200)");
          mCanvasDrawer.writeCenteredText("Klick auf mich!");
        }
        ...
      }
    

In diesem Beispiel wird eine Instanz der Klasse CanvasDrawer in der privaten Member-Variable mCanvasDrawer gespeichert. In der Funktion drawBackground() werden dann mehrere öffentliche Methoden des Objekts aufgerufen.

Private Functions ("private methods")

Werden Functions wie bisher nur mit function <Function-Name> definiert, so sind diese nur in der Klasse sichtbar. (Im Vergleich zu Java wären dies die Methoden, die mit dem Kenner private gekennzeichnet sind.)

Für den Aufruf innerhalb der Klasse reicht der Function-Name. Im folgenden ein Beispiel:

      function SampleApplication() {

        var mCanvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400);
        var mDrawingObjects = [];
        var mCurrentDrawingObjectId = 0;
        const MAX_DRAWING_OBJECTS = 20;
      
      
        this.firstDraw = function() {
          mCanvasDrawer.registerOnClick(this.addDrawingObject);
          draw();
        }
        ...
        function draw() {
          drawBackground();
          drawObjects();
        }
      
        function drawBackground() {
          mCanvasDrawer.clearCanvas();
          mCanvasDrawer.setColor("rgb(200, 200, 200)");
          mCanvasDrawer.writeCenteredText("Klick auf mich!");
        }
      
        function drawObjects() {
          for(let i = 0; i < mDrawingObjects.length; i++) {
            mDrawingObjects[i].draw(mCanvasDrawer);
          }
        }
    

In diesem Beispiel sieht man wie zunächst eine öffentliche Methode mit firstDraw definiert wird. Diese ruft dann die private Methode draw auf, welche wiederum zwei weitere private Methoden aufruft.

Wird die private Methode von außen aufgerufen, so kommt ein Fehler.

      sampleApplication =  new SampleApplication();
      sampleApplication.firstDraw();
      sampleApplication.drawBackground(); //Uncaught TypeError: sampleApplication.drawBackground is not a function
    

Die Funktion firstDraw kann ausgeführt werden, weil es eine öffentliche Funktion ist. Die Funktion drawBackground wird außerhalb der Klasse nicht erkannt.

Private Funktionen haben für mich folgende Vorteile:

Vererbung

In dieser Beispielanwendung habe ich mehrere Zeichenobjekte realisiert. Das Grundkonzept für ein Zeichenobjekt ist in der Klasse DrawingObject festgelegt.

      function DrawingObject(xCenter, yCenter) {

        //variables must be public otherwise child classes would not see them
        this.mXCenter = parseInt(xCenter);
        this.mYCenter = parseInt(yCenter);
      
        //dummy draw method
        this.draw = function(canvasDrawer) {
          console.log("The draw method has not been overwritten");
        }
      }
    

Diese Klasse wird initialisiert mit X- und Y-Koordinaten, die das Zentrum des Zeichenobjekts repräsentieren. Sie werden in öffentlichen Member-Variablen gespeichert. Diese sind öffentlich, weil sie ansonsten von vererbenden Klassen, den eigentlichen Zeichenobjekten, nicht gelesen werden können.

Zusätzlich gibt es dann noch eine Pseudo-Implementierung der Methode draw, welche das Zeichnen des Objekts realisieren soll. In dieser Klasse wird nur eine log-Meldung in die Konsole geschrieben, dass diese Methode überschrieben werden müsste.

Die Klasse Square ist nun eine Klasse, welche von DrawingObject vererbt.

      function Square(xCenter, yCenter) {

        //call parent constructor
        DrawingObject.call(this, xCenter, yCenter);
      
        this.draw = function(canvasDrawer) {
          //use members of parent
          canvasDrawer.drawRectangle(this.mXCenter-50, this.mYCenter-50, 100, 100, "rgb(255,0,0)");
        }
      }
    

Die Klasse Square übernimmt im Konstruktor die selben X- und Y-Koordinaten als Parameter. Das Verarbeiten dieser Koordinaten wird aber jetzt von der Klasse DrawingObject übernommen, in dem man mit call den Konstruktor dieser Klasse aufruft. Als erster Parameter wird this übergeben, damit die aktuelle Klasse (Square) als Besitzer der Eigenschaften und Methoden von DrawingObject bekannt gemacht wird. Danach folgen die eigentlichen Parameter des Konstruktors.

Danach wird die öffentliche Methode draw neu definiert und damit überschrieben. Diesmal wird wirklich das Zeichnen eines Quadrats umgesetzt.

In der Klasse Triangle wurde nun vergessen die Methode draw zu überschreiben:

      function Triangle(xCenter, yCenter) {

        //call parent constructor
        DrawingObject.call(this, xCenter, yCenter);
      
        //the draw method is not implemented!!!
      }
    

Da sie aber den Konstruktor von DrawingObject aufruft, besitzt diese Klasse auch alle öffentlichen Member-Variablen und alle öffentlichen Funktionen von DrawingObject.

Somit kann man fehlerlos folgenden Code ausführen.

      let drawingObject = new Square(0, 0);
      drawingObject.draw(canvasDrawer);

      drawingObject = new Triangle(0, 0);
      drawingObject.draw(canvasDrawer);
    

Allerdings wird beim ersten Aufruf von draw ein Quadrat gezeichnet. Beim zweiten Aufruf von draw wird eine Meldung in die Konsole geschrieben, dass die Methode implementiert werden muss.

Würde man nun in Triangle die Methode draw richtig implementieren, so könnte man Triangle Objekte genauso wie Square Objekte verwenden und je nachdem, um welches Objekt es sich handelt würde eine andere Figur gezeichnet werden.

Würde man noch komplexere Dinge in Square und Triangle umsetzen, so könnte man auch Dinge, die für beide gleich sind, in die Vater-Klasse DrawingObject auslagern.

Mein Fazit

Ich kämpfe persönlich leider mit dieser Syntax. Dabei hab ich folgende Probleme:

Sehr angenehm empfinde ich aber das Definieren von privaten Variablen und Methoden.

Hingegen ist dieses Konzept, um Vererbungen zu realisieren, etwas holprig. Vor allem wird nur indirekt über die call Methode angezeigt wird, von welcher Klasse vererbt wird.