Auf dieser Seite beschreibe ich meine Erfahrungen mit dem objektorientierten Programmieren in JavaScript mit classes, welche mit ECMAScript 2015 bzw. ES6 eingeführt worden ist. Zu diesem Thema habe ich mir eine kleine Link-Sammlung angelegt mit hilfreichen Tutorials:
Allgemeine Tipps zum Programmieren mit JavaScript findet man auf dieser Seite. Um objektorientiert zu
programmieren, ohne die class
-Syntax zu verwenden, habe ich die Seite
Objektorientiertes Programmieren in JavaScript mit functions erstellt.
Die folgende Beispiel-Anwendung ist in objektorientiertem JavaScript mit class
-Notation 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 hier wie bei anderen Programmiersprachen mit dem Keyword class
als solche
gekennzeichnet. Im folgenden Beispiel wird
die Klasse DrawingObject
mit zwei privaten Member-Variablen und drei öffentlichen Methoden definiert.
Die Klasse hat einen Konstruktor mit zwei Parametern, die dann in den Member-Variablen gespeichert werden.
class DrawingObject { #xCenter; #yCenter; constructor(xCenter, yCenter) { this.#xCenter = parseInt(xCenter); this.#yCenter = parseInt(yCenter); } getXCenter() { return this.#xCenter; } getYCenter() { return this.#yCenter; } //dummy draw method draw(canvasDrawer) { console.log("The draw method has not been overwritten"); } }
Will man nun die Klasse und deren öffentlich sichtbaren Methoden 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.getXCenter() + ", " + drawingObject.getXCenter()); drawingObject.draw();
Hier passieren folgende Dinge:
#xCenter
und #yCenter
gespeichert.
get
-Methoden in einer Meldung ausgegeben.
draw
des Objekts ausgeführt.
Meine Empfehlungen für Klassen:
Durch die Verwendung von Klassen ergeben sich folgende Vorteile:
Variablen, die nur in der Klasse sichtbar sein sollen, müssen einen Namen erhalten, der mit #
beginnt.
Diese Variablen können zur Übersichtlichkeit gleich nach der Klassendefinition deklariert werden. Ansonsten werden
sie deklariert, wenn im Konstruktor oder in einer Methode die Variable das erste Mal mit this.#<Variable>
erwähnt wird.
Leider ist es in Klassen nicht möglich Konstanten mit const
zu definieren. Wie ich das lösen würde, habe ich
im Abschnitt Private Konstanten beschrieben.
Werden die privaten Member-Variablen im Konstruktor oder in Funktionen dann verwendet, so muss man sie immer mit
this.#<Variable>
anführen. this
kennzeichnet dabei, dass es sich um Variablen der Klasse handelt.
Das folgende Beispiel zeigt, wie man private Member-Variablen deklariert, wie ihnen im Konstruktor Werte zugewiesen werden
und wie sie später in einer privaten Methode verwendet werden.
class SampleApplication { #canvasDrawer; #drawingObjects; #currentDrawingObjectId; constructor() { this.#canvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400); this.#drawingObjects = []; this.#currentDrawingObjectId = 0; } ... #addDrawingObject(drawingObject) { this.#currentDrawingObjectId++; if (this.#drawingObjects.length < this.#MAX_DRAWING_OBJECTS) { this.#drawingObjects.push(drawingObject); } else { this.#currentDrawingObjectId = this.#currentDrawingObjectId % this.#MAX_DRAWING_OBJECTS; this.#drawingObjects[this.#currentDrawingObjectId] = drawingObject; } } ... }
Werden die privaten Variablen außerhalb der Klasse aufgerufen, so kommt es zu einem Syntax-Fehler und die JavaScript-Logik würde überhaupt nicht ausgeführt werden.
let anotherSampleApplication = new SampleApplication(); alert(anotherSampleApplication.#currentDrawingObjectId); // --> SyntaxError: reference to undeclared private field or method #currentDrawingObjectId alert(anotherSampleApplication.#MAX_DRAWING_OBJECTS);
Das hat den Vorteil, dass Variablen, die nur für die interne Logik in Klassen benötigt werden, außerhalb der Klasse nicht verwendbar sind.
Wie im vorigen Abschnitt bereits angesprochen, kann man in den Klassen nicht einfach mit const
Konstanten definieren.
Zu diesem Thema habe ich dann
diese Diskussion
gefunden und für mich folgende Lösung für die Definition
von privaten Konstanten gefunden.
Um eine Konstante zu definieren, schreibt man eine get
Methode, die den Namen der privaten Konstante hat und keine Parameter
nimmt. Sie gibt lediglich den Wert der Konstanten zurück. Es handelt sich daher um eine Spezialform einer privaten Member Methode, was
im Abschnitt Private Functions ("private methods") ausführlicher beschrieben wird.
Im folgenden Code sieht man ein Beispiel:
get #MAX_DRAWING_OBJECTS() { return 20; }
Diese Methode kann man dann wie eine private Member-Variable verwenden.
#addDrawingObject(drawingObject) { this.#currentDrawingObjectId++; if (this.#drawingObjects.length < this.#MAX_DRAWING_OBJECTS) { this.#drawingObjects.push(drawingObject); } else { this.#currentDrawingObjectId = this.#currentDrawingObjectId % this.#MAX_DRAWING_OBJECTS; this.#drawingObjects[this.#currentDrawingObjectId] = drawingObject; } }
Würde man this.#MAX_DRAWING_OBJECT
nachträglich einen Wert zuweisen, so würde man einen TypeError erhalten und die
Ausführung der Logik wird an diesem Punkt abgebrochen.
this.#MAX_DRAWING_OBJECTS = 3; //TypeError: setting getter-only property #MAX_DRAWING_OBJECTS
Öffentlich sichtbare Member-Variablen deklariert man genau so wie die privaten Member-Variablen, allerdings muss man
das führende #
weglassen. Das heißt man kann unter der Klassendefinition einfach die Variablen anführen und
im Konstruktor bzw. in den Methoden der Klassen mti this.<Variable>
darauf verweisen.
In folgendem Code handelt es sich um das Beispiel aus dem Abschnit Private Variablen.
Ich habe lediglich die #
-Zeichen bei den Member-Variablen entfernt und dadurch
wären die Variablen auch außerhalb der Klasse sichtbar und könnten dort verwendet und verändert werden.
class SampleApplication { canvasDrawer; drawingObjects; currentDrawingObjectId; constructor() { this.canvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400); this.drawingObjects = []; this.currentDrawingObjectId = 0; } ... #addDrawingObject(drawingObject) { this.currentDrawingObjectId++; if (this.drawingObjects.length < this.#MAX_DRAWING_OBJECTS) { this.drawingObjects.push(drawingObject); } else { this.currentDrawingObjectId = this.currentDrawingObjectId % this.#MAX_DRAWING_OBJECTS; this.drawingObjects[this.currentDrawingObjectId] = drawingObject; } } ... }
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.
Für mein Beispiel, das ich im Abschnitt Vererbung beschreibe, muss ich Member-Variablen
in der Vater-Klasse definieren, die in den abgeleiteten Klassen lesbar sind. Dafür habe ich private Member-Variablen erstellt und
jede Member-Variable hat eine öffentliche get
-Methode erhalten. Dies ist hier zu sehen:
class DrawingObject { #xCenter; #yCenter; constructor(xCenter, yCenter) { this.#xCenter = parseInt(xCenter); this.#yCenter = parseInt(yCenter); } getXCenter() { return this.#xCenter; } getYCenter() { return this.#yCenter; } ... }
Somit ist sichergestellt, dass die Variablen zwar sichtbar sind, aber nicht außerhalb der Klasse verändert werden können.
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>, ...) {}
kann man dass Kennwort function
einfach weglassen.
Dabei empfiehlt es sich die Function-Namen mit Kleinbuchstaben beginnen zu lassen.
Im vorigen Abschnitt haben wir schon die einfachen
get
-Methoden für die Ausgabe von privaten Variablen gesehen. Hier nun ein weiteres Beispiel für öffentliche Methoden:
class CanvasDrawer { #height; #width; #context; ... //////////////////////////////// // PUBLIC METHODS // //////////////////////////////// clearCanvas(color = 'rgb(0, 0, 0)') { this.setColor(color); this.#context.fillRect(0, 0, this.#width, this.#height); } setColor(color) { this.#context.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, wobei
der Kenner this
wieder auf das Objekt hinweist, dem diese Funktion gehört.
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. Hier ist zunächst einmal ein Code-Beispiel:
class SampleApplication { #canvasDrawer; ... constructor() { this.#canvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400); ... } ... #drawBackground() { this.#canvasDrawer.clearCanvas(); this.#canvasDrawer.setColor("rgb(200, 200, 200)"); this.#canvasDrawer.writeCenteredText("Klick auf mich!"); } ... }
In diesem Beispiel befinden wir uns in der Klasse SampleApplication
, welche die Funktionalität der
Klasse CanvasDrawer
nützt. Dabei wird zunächst eine private Member-Variable namens #canvasDrawer
deklariert.
Im Konstrukter wird dieser Variable dann eine Instanz der Klasse CanvasDrawer
zugewiesen.
In der Methode #drawBackground()
werden dann mehrere öffentliche Methoden der Variable #canvasDrawer
aufgerufen.
Genauso wie bei öffentlichen und privaten Member-Variablen kann man mit einem führenden #
bei
Funktionsnamen entscheiden, ob es sich um öffentliche oder private Methoden der Klassen handelt.
Ein führendes #
kennzeichnet
also eine private Methode, die außerhalb der Klasse nicht sichtbar ist.
Innerhalb der Klasse kann man die Funktion aber wieder mit dem vorangestellten this.
aufrufen.
class SampleApplication { #canvasDrawer; #drawingObjects; #currentDrawingObjectId; constructor() { this.#canvasDrawer = new CanvasDrawer("SampleApplicationDiv", "SampleApplicationCanvas", 400); this.#drawingObjects = []; this.#currentDrawingObjectId = 0; } //////////////////////////////////// // PUBLIC METHODS // //////////////////////////////////// firstDraw() { this.#canvasDrawer.registerOnClick(this.addDrawingObject); this.#draw(); } ... //////////////////////////////////// // PRIVATE METHODS // //////////////////////////////////// #draw() { this.#drawBackground(); this.#drawObjects(); } #drawBackground() { this.#canvasDrawer.clearCanvas(); this.#canvasDrawer.setColor("rgb(200, 200, 200)"); this.#canvasDrawer.writeCenteredText("Klick auf mich!"); } #drawObjects() { for(let i = 0; i < this.#drawingObjects.length; i++) { this.#drawingObjects[i].draw(this.#canvasDrawer); } } }
In diesem Beispiel sieht man wie zunächst eine öffentliche Methode namens 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 und die JavaScript-Logik wird nicht ausgeführt.
sampleApplication = new SampleApplication(); sampleApplication.firstDraw(); sampleApplication.#drawBackground(); //Uncaught SyntaxError: reference to undeclared private field or method #drawBackground
Die Funktion firstDraw
kann ausgeführt werden, weil es eine öffentliche Funktion ist.
Der Aufruf der Funktion #drawBackground
stellt allerdings einen Syntax-Fehler dar!
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.
class DrawingObject { #xCenter; #yCenter; constructor(xCenter, yCenter) { this.#xCenter = parseInt(xCenter); this.#yCenter = parseInt(yCenter); } getXCenter() { return this.#xCenter; } getYCenter() { return this.#yCenter; } //dummy draw method draw(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 privaten Member-Variablen gespeichert. Für diese gibt es öffentliche get
-Methoden, damit
deren Werte in den
vererbenden Klassen, den eigentlichen Zeichenobjekten, 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.
class Square extends DrawingObject { draw(canvasDrawer) { //use members of parent canvasDrawer.drawRectangle(this.getXCenter()-50, this.getYCenter()-50, 100, 100, "rgb(255,0,0)"); } }
Die Klasse Square
erweitert mit dem Kennwort extends
die Klasse DrawingObject
.
Somit erhält diese neue Klasse automatisch den selben Konstruktor und die selben öffentlichen Methoden wie DrawingObject
.
Um jetzt aber eine sinnvolle draw
Methode zu schreiben, wird diese in der Klasse Square
einfach neu
geschrieben und damit die Funktionalität, die von DrawingObject
geerbt wurde, überschrieben.
Leider kann man in dieser Methode nicht mehr auf die privaten Member-Variablen von DrawingObject
zugreifen, weshalb die
öffentlichen get
-Methoden zum Abfragen der Koordinaten zum Einsatz kommen.
In der Klasse Triangle
wurde nun vergessen, die Methode draw
zu überschreiben:
class Triangle extends DrawingObject { //the draw method is not implemented!!! }
Da sie aber auch von DrawingObject
vererbt, besitzt diese Klasse ebenfalls den selben Konstruktor und die selben öffentlichen
Methoden.
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 finde das Konzept der class
-Syntax in JavaScript viel besser als die Syntax mit function
-Syntax, welche ich
hier beschrieben habe.
class
.
#
erkennbar.
this
benötigt,
um sie ansprechen zu können.
Leider zögerten manche Browser wie Firefox lange damit, private Member-Variablen und Methoden zu unterstützen.
In Firefox wurde dies erst mit
Version 90 im
Juli 2021 unterstützt. Davor kam es zu Fehlern, wenn man private Member-Variablen bzw. private Methoden mit dem führenden #
definiert hat.