Geschrieben von

OOP in JavaScript

WebDev

“OOP” steht für “Objektorientierte Programmierung”. In diesem Beitrag gebe ich eine kurze Einführung in die objektorientierte Programmierung innerhalb JavaScript. Zunächst gehe ich auf ein paar Hintergrundinformationen ein. Danach zeige ich anhand eines JavaScript-Beispiels, was Objektorientierung in der Praxis bedeutet.

Die Programmierparadigmen

Beim einem Programmierparadigma handelt es sich um die Art und Weise, wie man ein Programm schreibt. Grob unterscheidet man dabei in:

  • Deklarative Programmierung: Deklarativ bedeutet, dass der Entwickler schreibt, was zu tun ist.
  • Imperative Programmierung: Imperativ bedeutet, dass der Entwickler schreibt, wie etwas zu tun ist.

Innerhalb der deklarativen Programmierung unterscheidet man dann weiter in:

  • Logische Programmierung: Dieses Paradigma basiert auf mathematischen Logiken und wird primär in der Forschung angewendet. Ein logisches Programm basiert auf einer Ansammlung von Fakten und Annahmen. Stellt man hier eine Anfrage, dann berechnet der Interpreter die Lösung anhand der Fakten und Annahmen.
  • Funktionale Programmierung: Bei diesem Paradigma werden die Programme als eine Reihe von Funktionen strukturiert. Die Funktionen haben Ein- und Ausgaben und werden der Reihe nach ausgeführt.

Dann gibt es noch die imperative Programmierung, die weiter unterteilt werden kann in:

  • Strukturierte Programmierung: Hier steht die Programmstruktur an oberster Stelle. Dabei wird das Programm in Teilprogramme zerlegt. Charakteristisch ist, dass Programme hintereinander ausgeführt werden und mit Verzweigungen und Schleifen umgesetzt werden.
  • Prozedurale Programmierung: Auch hier wird das Programm so strukturiert, dass es hintereinander (also sequentiell) ausgeführt wird, was mit der strukturierten Programmierung übereinstimmt. Unterschied ist jedoch, dass bei der prozeduralen Programmierung wiederkehrende Befehle in Funktionen (hier Prozeduren genannt) ausgelagert werden.
  • Objektorientierte Programmierung: Bei der objektorientierten Programmierung wird alles als Objekt angesehen. Jedes Objekt hat Eigenschaften und Methoden.

Um das objektorientierte Paradigma geht es in diesem Beitrag. Dabei gehe ich auf die Objektorientierung in JavaScript ein. Zu beachten ist, dass das nicht bedeutet, dass JavaScript rein zur objektorientierten Programmierung gezählt werden darf. Viele Sprachen besitzen Elemente verschiedener Paradigmen.

Objektorientierte Programmierung (OOP) an einem Praxisbeispiel erklärt

Um das nachfolgende Beispiel zu verstehen, solltest du dich mit Funktionen in JavaScript schon vertraut gemacht haben. Schaue dir sonst gerne meinen Beitrag mit dem Titel “JavaScript: function()” an.

Gehen wir also zunächst von einer Funktion aus, die in JavaScript wie folgt geschrieben wird:

function fuehreAus () {
// Anweisung
}

Nun kann man in den runden Klammern der Funktion einen Parameter übergeben. Dieser Parameter landet dann in der Funktion selbst, mit der die Funktion dann auch weiterrechnet und später einen Wert ausgibt. Hier ein Beispiel:

let summe = function (a, b) {
return a + b;
}
let ergebnis = summe(1, 2);

Dabei stehen die Funktion und der übergebene Parameter nicht in Zusammenhang, sind also unabhängig voneinander. Dies wäre nun der prozedurale Ansatz. Wie wir oben gelernt haben, werden die Befehle bei der prozeduralen Programmierung nacheinander abgearbeitet, wobei wiederkehrende Befehle in Funktionen gespeichert werden.

Bei der objektorientierten Programmierung können jedoch die Funktionen in Zusammenhang mit den Parametern bzw. Objekten stehen. Das heißt: Das Objekt muss nicht als Parameter übergeben werden. Stattdessen ist der Parameter Bestandteil des Objekts. In der Objektorientierung stehen diese Objekte im Mittelpunkt. Sie besitzen Methoden und Eigenschaften. Die Eigenschaften besitzen wiederum bestimmte Informationen über die Objekte. Mehr zu den Objekten später mehr.

Schauen wir uns den Unterschied zwischen prozeduraler und objektorientierter Programmierung an einem Codebeispiel nochmal näher an.

Gehen wir von einer Funktion aus, die für ein Fortbewegungsmittel bestimmt wie gefahren werden soll:

function fahren () {
// Anweisung, wie man fährt
}

Möchten wir nun ein Auto und Moped fahren lassen, müssten wir das wie folgt ausführen:

fahren (auto);
fahren (moped);

Problem ist dabei, dass es einen Unterschied macht, ob man ein Auto oder Moped fahren möchte. Dementsprechend müsste auch die Funktion angepasst werden. Beide lassen sich nicht gleich fahren, was konkret heißt: Sie können nicht die gleiche Funktion ausführen bzw. teilen. Daher müsste man wie folgt die Funktion anpassen:

function fahren () {
if (auto) {
// Anweisung, wie man ein Auto fährt
} else if (moped) {
// Anweisung, wie man ein Moped fährt
}
}

Jetzt kann es sein, dass immer mehr Fortbewegungsmittel wie Fahrrad, LKW, Flugzeug, etc. dazukommen können. Dementsprechend müsste die Funktion um mehrere else-if-Anweisungen erweitert werden. Dies wird aber schnell unübersichtlich. Für diesen Zweck kann also der prozedurale Ansatz nicht der geeignete Weg sein.

Daher geht man in der Objektorientierung einen anderen Weg. Auto und Moped bekommen ihre eigene, individuelle Funktion. Statt der oberen Funktionsschreibweise, würde die Funktion wie folgt aufgerufen werden:

auto.fahren():
moped.fahren();

Was ist objektorientierte Programmierung (OOP)?

Am obigen Beispiel kann man sagen, dass uns OOP ermöglicht, unseren Code zu strukturieren, indem wir das zusammenfassen, was zusammengefasst gehört und das wir Objekte dabei verwenden.

In der Theorie gibt es jedoch einige Grundelemente und in der Praxis einige Konzepte, die OOP in JavaScript ausmachen. Diese schauen wir uns im nächsten Schritt an, um einen umfassenden Überblick und ein näheres Verständnis über Objektorientierung zu erlangen.

Grundelemente der Objektorientierung

Die Grundelemente geben einen Einblick nach welchen Prinzipien die Objektorientierung aufgebaut ist. Das Verständnis darüber ist wichtig, um in der Praxis die richtigen Entscheidung zu treffen, wenn es um die Code-Struktur geht. Auf folgenden Grundelementen basiert die Objektorientierung:

  • Datenkapselung
  • Polymorphie
  • Vererbung
  • Abstraktion

Datenkapselung
In der objektorientierten Programmierung gruppiert man ähnliche Variablen und Funktionen, die mit diesen Variablen arbeiten, in Objekte. Das wird Kapselung bezeichnet. Nachfolgend ein Beispiel. Gehen wir vom folgenden Code aus:

let gehalt = 100000;
let ueberstunden = 20;
let stundensatz = 80;
 
function rechneGehalt (gehalt, ueberstunden, stundensatz) {
  return gehalt + (ueberstunden * stundensatz);
}

Das ist klassische prozedurale Programmierung. In der objektorientierten Programmierung würde man es jedoch wie folgt lösen:

let mitarbeiter = {
  gehalt = 100000,
  ueberstunden = 10,
  stundensatz = 80,
  rechneGehalt: function() {
    return this.gehalt + (this.ueberstunden * this.stundensatz);
  }
};
mitarbeiter.rechneGehalt();

Hier sehen wir, dass wie ein Mitarbeiter-Objekt haben. Es sind 3 Eigenschaften enthalten und eine Methode. Bei der Funktion sieht man zudem, dass diese keine Parameter hat, während sie bei der prozedurale Programmierung 3 Parameter entgegennimmt. Grund ist, dass bei der objektorientierten Programmierung die Daten gekapselt sind (in einem Objekt) und so eine Einheit bilden. In OOP haben die Funktionen deutlich weniger Parameter.

Abstraktion
Stelle dir ein Smartphone vor, wo du ein paar Tasten hast mit denen du Programme steuern kannst. Du drückst auf “Play” und Musik wird abgespielt. Du drückst auf “Senden” und die Mail wird abgeschickt. Im Hintergrund passiert aber viel mehr. Damit die Funktionen durchgeführt werden können, muss einiges an Code verarbeitet werden. Die ganze komplexe Logik wird hinter einem Button “versteckt”. Das nennt man Abstraktion. Die selbe Technik lässt sich in Objekten nutzen. Eigenschaften und Methoden können von außen versteckt werden. Dadurch wird das Interface einfacher, da nur die “einfachen” Methoden und Eigenschaften zur Verfügung gestellt werden. Zudem ist durch Abstraktion der Code weniger anfällig für Änderungen. Wenn sich der komplexe Code ändert, kann der einfache Code unverändert bleiben. Bei Klick auf “Play” wird immer Musik abgespielt. Wenn der Entwickler jedoch den dahinterliegenden, komplexen Code ändert, bleibt die Play-Funktion dennoch bestehen.

Vererbung
Mit Vererbung kann redundanter Code eliminiert werden. Zum Beispiel haben HTML-Elemente die Eigenschaft “innerHTML”. Damit nicht jeder dieser Elemente einen eigenen innerHTML-Code aufweisen muss, kann der Code ausgelagert werden und alle HTML-Elemente greifen nur darauf zu.

Polymorphie
Polymorphie kann auch mit “Vielgestaltigkeit” übersetzt werden. Ein Objekt kann also verschiedene Formen annehmen.

Konzepte der Objektorientierung

In JavaScript gibt es verschiedene Konzepte, die man für eine objektorientierte Programmierung einsetzen kann. Diese sind:

  • Objekte
  • Konstruktoren
  • Klassen
  • Prototypen

Objekte

Objekte sind ein Datentyp in JavaScript, die Object genannt werden. Man kann sich Objekte wie eine Liste vorstellen, die verschiedene Werte enthält. Genauer gesagt lassen sich in Objekten Eigenschaften und Methoden speichern. Objekte können mit “new Object();” oder mit der Literalschreibweise erzeugt werden. Möchte man Daten abrufen, so geschieht das entweder über die Punktschreibweise oder über die eckigen Klammern.

Hier ein Beispiel eines einfachen Objekts:

var fahrzeug = {
farbe: "schwarz",
ps: 300,
geschwindigkeit: 340,
baujahr: 2010
};

Mehr über Objekte erfährst du in meinem Beitrag “JavaScript: Objekte”.

Klassen

Wenn wir nun das obige Objekt auf verschiedene Fahrzeuge anwenden möchten, so wäre es gut, wenn man eine Vorlage bzw. ein Muster besitzt. Bei Fahrzeugen könnte man unterscheiden in Auto, Moped, LKW, etc. Alle diese Fahrzeuge haben bestimmte Gemeinsamkeiten (Farbe, PS, Baujahr, etc.).

Hier kommen die Klassen ins Spiel, die man sich eben als eine Art Muster vorstellen kann. Sie werden einmal definiert und können anschließend nach dem gleichen Muster verwendet werden.

Bis ES6 (ECMAScript 2015) gab es in JavaScript keine klassische Klassen-Definition. Es wurde mit einer Funktion zunächst eine Klasse definiert (1. Schritt):

function Fahrzeug(Farbe) {
this.Farbe = Farbe;
}

Klassen konnte man daran erkennen, dass der Anfangsbuchstabe von den meisten Entwicklern groß geschrieben worden ist. Um im nächsten Schritt eine Klasse zu initialisieren (2. Schritt), musste man das Schlüsselwort new einsetzen:

function Fahrzeug(Farbe) {
this.Farbe = Farbe;
}
var Moped = new Fahrzeug("rot"); // Klassen-Initialisierung
console.log(Moped.Farbe); // Zugriff auf die Eigenschaft

Mit der Definition der Klasse erstellen wir also ein Muster. Im Anschluss wird die Klasse initialisiert. Das geschieht mit dem Schlüsselwort new, wodurch ein neues Objekt entsteht. new ist dabei auch eine Funktion, die Konstruktor genannt wird. Das Objekt, welches mit dem Konstruktor new erzeugt worden ist, wird auch als Instanz bezeichnet.

Mit ES6 wurde jedoch das Schlüsselwort class eingeführt, mit denen man nun Klassen deklarieren kann. Dabei ist class nichts weiter als eine Funktion. Wenn wir bei unserem obigen Beispiel mit der Fahrzeug-Funktion bleiben, dass haben wir mit class folgende Sytax:

class Fahrzeug {
constructor (Farbe) {
this.Farbe = Farbe;
}
}

Statt function wird hier also class verwendet und die Eigenschaften werden in der constructor-Methode zugewiesen. Man muss bei Klassen nur beachten, dass kein Hoisting stattfindet wie bei klassischen Funktionen. Bevor Klassen benutzt werden, müssen sie also deklariert werden.

Trotz der Einführung der Klassen-Definition bleibt JavaScript eine prototypbasierte Sprache (weiter unten dazu mehr).

Konstruktoren

Konstruktoren sind nichts weiter als Funktionen, die Objekte erzeugen. Die erzeugten Objekte werden dann als Instanzen – sozusagen Abkömmlinge – genannt. Mit Konstruktoren können also unzählige und gleiche Objekte bzw. Instanzen erstellt werden.

Um Konstruktoren genauer zu erklären, schauen wir uns das erzeugte Objekt von oben nochmal an:

var fahrzeug = {
farbe: "schwarz",
ps: 300,
geschwindigkeit: 340,
baujahr: 2010
};

Es handelt sich um ein sehr einfaches Objekt mit Eigenschaften. Fügen wir nun dem Objekt eine Methode hinzu:

var fahrzeug = {
farbe: "schwarz",
ps: 300,
geschwindigkeit: 340,
baujahr: 2010,
bremsen: function() {
  console.log("bremsen");
}
};

Wenn wir nun ein weiteres Fahrzeug erstellen möchten, müssten wir den Code kopieren. Das kann aber zu einem Problem werden, vor allem wenn wir mehrere Methoden im Objekt haben. Die Bremsen-Funktion würde dann an 2 Stellen sein. Ändert sich was an der Methode, dann muss man es an verschiedenen Stellen anpassen. Hat das Objekt zudem mehrere Methoden, dann sind Anpassungen noch aufwendiger. Hat ein Objekt also min. eine Methoden, dann ist diese Objekt-Schreibweise (genauer gesagt handelt es sich um die Literalschreibweise) nicht die beste Wahl. Die Lösung kann die Verwendung von Factory-Funktionen sein. Das obige Beispiel würde dann wie folgt aussehen (hier nur vereinfacht mit einer Eigenschaft):

function fahrzeugErstellen(farbe) {
  return {
    farbe: farbe,
    bremsen: function() {
      console.log("bremsen");
    }
};

Hier spricht man dann von einer Factory-Funktion. Ein Auto kann dann wie folgt erstellt werden:

const fahrzeug = fahrzeugErstellen("schwarz");

Es gibt noch eine weitere Methode ein Objekt zu erzeugen: Die Verwendung von Konstruktor-Funktionen. Diese würde wie folgt aussehen:

function Fahrzeug(farbe) {
  this.farbe = farbe;
  this.bremsen = function() {
    console.log("bremsen");
  }
}

In JavaScript werden die Namen der Konstruktor-Funktionen mit Großbuchstaben begonnen. Im Body der Funktion wird statt einer Rückgabe (return) das Keyword this verwendet. Dabei handelt es sich um eine Referenz auf das Objekt, welches den Code ausführt. Um ein neues Fahrzeug zu erstellen wird dies wie folgt erreicht:

const blauAuto = new Fahrzeut("blau");

Durch den Einsatz des new-Keywords passieren nun 3 Sachen:

  • Zunächst wird ein leeres Objekt erstellt {}
  • new setzt dann this, damit es auf das Objekt zeigt
  • Zum Schluss wird ein Objekt auf Basis der Funktion zurückgegeben

Bez. dem letzten Punkt kann man erahnen, dass man kein “return” braucht. Das passiert automatisch mit dem new-Operator.

Prototypen

JavaScript ist eine prototypbasierte Sprache. Das heißt, dass jedes Objekt über die Eigenschaft prototype verfügt, mit der Eigenschaften und Methoden eines Objekts bei der Initialisierung erweitert werden können. Die Vorlage bzw. das Muster oder auch der Bauplan stellt also der Prototyp dar, mit dem Objekte erzeugt werden.

Der Unterschied zwischen klassenbasierter Sprache (Java oder C++) und prototypbasierter Sprache (JavaScript oder ActionScript) ist also grob wie folgt:

  • In der klassenbasierten Sprache sind Klassen das Muster für Objekte.
  • In der prototypbasierten Sprache sind Prototypen das Muster für Objekte.

Auch wenn man in JavaScript Klassen definieren kann – vor ES6 über Funktionen, nach ES6 mit class – so sind Klassen in JavaScript als syntaktischer Zucker zu verstehen. Prototypen bleiben also dennoch der Kern bei JavaScript.

Was ist aber der Unterschied zwischen diesen zwei Ansätzen?
Der Prototyp ist genau genommen ein prototypisches Objekt. Die Vorlage ist also selbst ein Objekt. Das heißt auch, dass beliebige Operationen auf dem Objekt ausgeführt werden können, während das bei Klassen nicht möglich ist.

Wenn man also aus einer Klasse ein Objekt bilden möchte, dann werden die Eigenschaften und Methoden so erstellt, wie es in der Klasse (Vorlage) festgelegt ist. Das Ergebnis ist die Instanz.

Wenn man aus einem Prototyp ein Objekt bilden möchte, dann referenziert das erzeugte Objekt auf den Prototypen. Über das neue Objekt können die Eigenschaften und Methoden im Prototyp erreicht werden. Hinzu kommt, dass das Prototyp-Objekt geändert werden kann. Eigenschaften und Methoden können hinzugefügt werden. Die nachträgliche Erweiterung unterscheidet Prototypen somit von Klassen.