Hier beschreibe ich meine Erfahrungen mit dem objektorientierten Programmieren in JavaScript mit functions. Zur Erstellung dieser Seite haben mir folgende Links geholfen:
call
Methode, die beim Vererben von Klassen angewandt wird.
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.
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.
Diese Anwendung verwendet folgende JavaScript-Dateien:
draw
Methode, die allerdings nur ein Konsolen-Logging absetzt.
draw
Methode nicht implementiert.
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:
mXCenter
und mXCenter
gespeichert.
draw
des Objekts ausgeführt.
Meine Empfehlungen für Klassen:
Durch die Verwendung von Klassen ergeben sich folgende Vorteile:
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.
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.
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.
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:
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.
Ich kämpfe persönlich leider mit dieser Syntax. Dabei hab ich folgende Probleme:
class
zu kennzeichnen.
Hier wird eine Klasse mit Hilfe von function
definiert, was für mich nicht intuitiv ist.
this.<Function-Name> = function(<Parameter1>, <Parameter2>, ...) {}
finde ich
sehr umständlich. (Wenn man sich damit beschäftigt, ist es aber logisch, dass damit eine Eigenschaft dieser
Klasse definiert wird, welche in diesem Fall eine Funktion ist.)
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.