JavaScript/Tutorials/OOP/Klassen und Vererbung
Text-Info
-
30min
- mittel
-
Kenntnisse in
● JavaScript
● Einstieg in die OOP
Eine Klasse (man denke an „klassifizieren“ und „Klassifikation“, nicht an „Klassenzimmer“) ist eine Vorlage, die verwendet wird, um neue Objekte zu erstellen, und diese Objekte werden Instanzen genannt. Objekte und Instanzen sind ein und dieselbe Sache und können austauschbar verwendet werden (und das sind sie oft!). Abgesehen davon deutet das Wort "Instanz" normalerweise auf Objekte hin, die aus einer Klasse mit dem new()-Operator erzeugt wurden, während "Objekt" ein allgemeinerer Begriff für alle Objekte ist.
Da Instanzen von Klassen kommen und Klassen Instanzen erzeugen, sind sie leichter zu verstehen, wenn man sie beide gleichzeitig diskutiert, also gehen wir zum nächsten Abschnitt, Instanzen...
Dabei führt es kein neues OOP-Modell in die Sprache ein, sondern ist syntaktischer Zucker für das bestehende, auf Prototypen basierende, Vererbungsmodell von JavaScript. Eine Klasse ist immer im strict mode, auch ohne ein "use strict"
am Anfang.
Inhaltsverzeichnis
- 1 class
- 2 Prototypische Vererbung (ES3)
- 2.1 Konstruktor-Funktionen (Konstruktoren)
- 2.2 Prototypische Objekte (Prototypen)
- 2.3 Vererbung vom Prototypen zur Instanz
- 2.4 Prototypen verstehen: Die Glasplatten-Metapher
- 2.5 Private Objekte anstatt private Eigenschaften
- 2.6 Nachteile von privaten Objekten
- 2.7 Wozu Kapselung gut ist und wann sie nötig ist
- 3 Weblinks
class[Bearbeiten]
Wir haben schon gelernt, wie man mit Konstruktorfunktionen Objekte erzeugen kann. Dabei können verschiedene Konstruktorfunktionen verschiedene Objekttypen erzeugen. Eine Klasse ist nun eine etwas ausgefeiltere Art, einen Bauplan für ein Objekt zu erstellen. Außerdem leistet sie noch mehr, aber dazu mehr im Bereich Vererbung.
Details: caniuse.com
Klassendeklaration[Bearbeiten]
Analog zu Funktionsausdrücken und Funktionsdeklarationen hat die Klassensyntax zwei Komponenten:
class Person {
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
}
var Person = class {
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
}
Wir schreiben im Grunde nichts Neues, sondern verwenden die identische Objektstruktur unseres Beispiels aus dem letzten Kapitel. Nur die Schreibweise ist etwas anders. Wir finden jedoch unsere Konstruktorfunktion wieder, hier unter dem festgelegten Namen constructor
.
Klassen werden, anders als die meisten Deklarationen in Javascript, nicht nach oben gehoben (Hoisting) und müssen daher vor der ersten Benutzung deklariert werden. Sie können auch nicht überschrieben werden.
Wie Sie mit typeof überprüfen können, ist Person
eine Funktion; das instantierte Objekt ann
jedoch ein Objekt.
Öffnen Sie die Konsole mit F12.
const ann = new Person('Ann', 'Mustermann', 31);
console.log("typeof Person = "+typeof Person);
console.log("typeof ann = "+typeof ann);
Die Parameter der Konstruktor-Methode können beliebig genannt werden und sind unabhängig von den Klassen-Variablen.
Methoden definieren[Bearbeiten]
Einfache Methoden[Bearbeiten]
Um eine Klasse funktionsfähig zu machen, müssen innerhalb der Klasse Methoden definiert werden. Eine Methode ist eine Funktion innerhalb einer Klasse.
class Person {
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
sayName = function () {
return (`My name is ${this._firstName} ${this._lastName} and I am ${this._age}!`)
}
}
const frizzi = new Person('Frizzi', 'Frisch', 15);
console.log(frizzi.sayName());
Anders als bei unserem Beispiel im letzten Kapitel wird hier die Methode sayName
nicht innerhalb der Konstruktorfunktion als Funktionsliteral notiert (was durchaus auch möglich wäre), sondern wird ganz klar als Methode neben dem Konstruktor aufgeführt. Das erhöht die Lesbarkeit und macht den Code übersichtlicher.
Um Werte von Klassen-Variablen zu ändern, muss eine Methode mit Parametern definiert werden.
class Person {
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
sayName = function () {
return (`My name is ${this._firstName} ${this._lastName} and I am ${this._age}!`)
}
alter (value) {
this._age = value;
}
}
Hier ändert die Methode alter
den Inhalt der Klassen-Variablen this._age
Der Name der Klassen-Variablen this._age
darf nicht mit dem Namen der Methode alter
übereinstimmen.
Methoden für Events[Bearbeiten]
Soll eine Methode durch einen Event (click, keydown ...) aufgerufen werden, ist zu beachten, dass innerhalb der Methode die Variable this
nicht auf die Klasse zeigt, sondern auf das Event-Objekt. Damit ist eine Klassen-Variable, die zB. mit this._alter
deklariert wurde, nicht mehr erreichbar. Um trotzdem auf die Variablen der Klasse zugreifen zu können, wird dem Event ein Objekt angeboten, dass die Eigenschaft handleEvent
besitzt. Weitere Eigenschaften sind beliebig, eine Eigenschaft für this
oder Eigenschaften für Klassen-Variablen müssen vorhanden sein, wenn diese in der bei handleEvent
angegebenen Methode benutzt werden sollen.
class Person {
constructor (name, alter) {
this._name = name;
this._alter = alter;
// object to handle event:
this.click = {handleEvent:this.hit, self:this};
}
vorstellen () {
return "Ich heiße " + this._name + " und bin " + this._alter + " Jahre alt.";
}
alter (value) {
this._alter = value;
}
hit(event) {
var classThis = this.self;
return "Ich bin " + classThis._alter + " Jahre alt.";
}
}
In der Methode hit
zeigt this
auf das Objekt this.click
Um die Methode hit
aufzurufen, wird dem HTML-Objekt mit der Id "alter" ein EventListener hinzugefügt.
var p = new Person('Frizzy', 15);
var object = document.getElementById("alter");
object.addEventListener("click",p.click);
Ein direkter Aufruf der Methode p.hit
führt zu einer Fehlermeldung.
Vererbung mit extends[Bearbeiten]
Man kann Klassen zueinander in Beziehung setzen, indem man das Modell der Vererbung anwendet: Eine Klasse vom Typ B kann die Objektstruktur einer Klasse vom Typ A erben und für sich erweitern. Damit bauen Objekte vom Typ B auf der Objektstruktur von Typ A auf.
class Person {
constructor(firstName, lastName, age) {
this._firstName = firstName;
this._lastName = lastName;
this._age = age;
}
sayName () {
return (`My name is ${this._firstName} ${this._lastName} and I am ${this._age}!`);
}
}
class Student extends Person {
tuWas () {
return "keine Lust";
}
}
class Teacher extends Person {
tuWas () {
return "korrigiere Klausur";
}
}
const frizzy = new Student('Frizzy', 'Frisch', 15);
write (frizzy.sayName());
write (frizzy.tuWas());
In diesem Beispiel werden drei Klassen definiert, wobei die erste Klasse (Person
) für die beiden folgenden Klassen eine Basis bildet, von der sie erben werden.
Die Klassen Student
und Teacher
übernehmen („erben”) die Objektstruktur von Person
mithilfe des Schlüsselwortes extends
(englisch für erweitert). Das ist der Grund, warum die Methode sayName
in den Klassen Student
und Teacher
ebenso verfügbar ist. Zusätzlich fügen beide jeweils noch eine eigene Methode hinzu, die hier in beiden Fällen tuWas
lautet, jedoch unterschiedliche Funktionalität hat.
ToDo (weitere ToDos)
Kapselung mit static[Bearbeiten]
Das Schlüsselwort static legt eine statische Methode für eine class
fest.
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/static
- https://www.sitepoint.com/javascript-private-class-fields/
Prototypische Vererbung (ES3)[Bearbeiten]
ToDo (weitere ToDos)
Abgleichen und, wenn nötig, in oberen Text integrieren; sonst depublizieren.
--Matthias Scharwies (Diskussion) 16:39, 14. Nov. 2021 (CET)Vor dem Einsatz der class-Syntax in ES6 wurde die klassenbasierte Vererbung mithilfe von Konstruktoren (wie im letzten Kapitel vorgestellt) und prototypischen Objekten (kurz: Prototypen) realisiert.
Mit Object-Literalen und dem Revealing Module Pattern haben wir das gebaut, was in anderen Programmiersprachen Singleton oder Klasse mit statischen Methoden genannt wird. Ein solches Modul kommt nur einmal vor und kann nur einen internen Status haben. In vielen Fällen ist es jedoch sinnvoll, mehrere Instanzen (Exemplare) eines Objektes zu erzeugen.
Konstruktor-Funktionen (Konstruktoren)[Bearbeiten]
Information
new
aufgerufen werden. Das neu erzeugte Instanzobjekt ist darin über
this
verfügbar.EineKonstruktor-Funktion ist ein Erzeuger neuer Objekte. Sie ist jedoch keine besondere Sprachstruktur, sondern erst einmal eine ganz normale Funktion. Zu einem Konstruktor wird sie lediglich dadurch, dass sie mit dem Schlüsselwort new aufgerufen wird.
Wenn eine Funktion mit new
aufgerufen wird, wird intern ein neues, leeres Object-Objekt angelegt und die Funktion im Kontext dieses Objektes ausgeführt. Das bedeutet, im Konstruktor kann das neue Objekt über this angesprochen werden. Darüber können ihm z. B. Eigenschaften und Methoden hinzugefügt werden.
Der Unterschied beim Konstruktor ist, dass auf diese Weise unzählige gleich ausgestattete Objekte, sogenannte Instanzen erzeugt werden können.
// Konstruktorfunktion
function Katze (name, rasse) {
// Zugriff auf das neue Objekt über this,
// Hinzufügen der Eigenschaften und Methoden
this.name = name;
this.rasse = rasse;
this.pfoten = 4;
}
const maunzi = new Katze('Maunzi', 'Perserkatze');
Die dem Konstruktor übergebenen Parameter werden zu Eigenschaften des Instanzobjekts. Auch wenn verschiedene Katze
-Instanzen abweichende Eigenschaftswerte haben können, so ist ihnen allen gemein, dass sie die Eigenschaften name
und rasse
besitzen. Neben diesen beiden gibt es eine feste Eigenschaft pfoten
: Katzen haben (in der Regel) vier Pfoten.
Eigenschaften können nach dem Erzeugen neue Werte bekommen. Der Zugriff von außen auf die Objekteigenschaften erfolgt über das bekannte Schema instanzobjekt.member
. Wie gesagt sind Objekte in JavaScript jederzeit änderbar und erweiterbar.
var maunzi = new Katze('Maunzi', 'Perserkatze');
alert(maunzi.name + ' ist eine ' + maunzi.rasse);
maunzi.rasse = 'Siamkatze';
alert(maunzi.name + ' ist neuerdings eine ' + maunzi.rasse);
Da diese Eigenschaften von außen zugänglich und schreibbar sind, handelt es sich um öffentliche Eigenschaften.
Prototypische Objekte (Prototypen)[Bearbeiten]
Information
Erweitern Sie dieses, so vererben sich dessen Eigenschaften auf alle Instanzen, die mit der Funktion erzeugt werden.
In den obigen Beispielen haben wir dem neuen Objekt direkt im Konstruktor Eigenschaften und Methoden hinzugefügt. Das bedeutet, dass diese Objekte mit jeder Instanz neu angelegt werden. Das ist eine Möglichkeit, wie dem Objekt Funktionalität hinzugefügt werden kann. Sie ist unter anderem dann notwendig, wenn Konstruktor-Parameter an das Instanzobjekt kopiert werden, wie es im Beispiel mit this.name = name;
getan wird.
Mit einer Funktion ist immer ein prototypisches Objekt (Prototyp) verknüpft. Es handelt sich um ein gewöhnliches allgemeines JavaScript-Objekt, wie wir es auch mit new Object()
oder dem Object-Literal {}
anlegen können. Dieser Prototyp ist bei eigenen Funktionen anfangs leer.
Über die Eigenschaft prototype
können wir ausgehend vom Funktionsobjekt auf den Prototypen zugreifen. Dieses Objekt können wir entweder erweitern oder mit einem eigenen Objekt ersetzen. Im folgenden Beispiel wird der Prototyp erweitert, indem eine Methode hinzugefügt wird:
function Katze () {}
Katze.prototype.miau = function () {
alert("Miau!");
};
var maunzi = new Katze();
maunzi.miau();
Hier wird ein Funktionausdruck notiert und das Funktionsobjekt in Katze.prototype.miau
gespeichert. Beim Prototypen wird also eine Eigenschaft namens miau
angelegt. Darin steckt nun die neu angelegte Funktion. Wenn wir eine Katze
-Instanz erzeugen, so besitzt sie eine miau
-Methode.
Vererbung vom Prototypen zur Instanz[Bearbeiten]
Information
Der Prototyp steht »hinter« einem Objekt:
Wird bei dem Objekt eine Eigenschaft nicht direkt gefunden, so kann der Prototyp einspringen und sie bereitstellen.Durch den Aufruf von new Katze
wird wie gesagt intern ein zunächst leeres Object erzeugt. Der Prototyp des Konstruktors, Katze.prototype
, wird dabei in die sogenannte Prototyp-Kette (Prototype Chain) des Objektes eingehängt. Dies ist eine geordnete Liste mit Objekten, die abgearbeitet wird, wenn auf eine Eigenschaft des Objektes zugegriffen wird.
Ein konkretes Beispiel: Wenn wir den Ausdruck maunzi.miau
schreiben, dann arbeitet der JavaScript-Interpreter die Prototyp-Kette ab, um die Eigenschaft namens miau
zu finden und damit den Ausdruck aufzulösen. Die Prototyp-Kette von maunzi
hat folgende Einträge:
maunzi
– das Instanzobjekt selbstKatze.prototype
– der Prototyp für alle Objekte, die mit demKatze
-Konstruktor erzeugt wurdenObject.prototype
– der Prototyp, der hinter allen Objekten steht
Das folgende Diagramm zeigt Objekte der Prototyp-Kette und listet deren Eigenschaften auf:
maunzi | |
---|---|
[[Prototype]] | Katze.prototype |
constructor | Katze |
Katze.prototype | |
---|---|
[[Prototype]] | Object.prototype |
miau | function () { alert("Miau!"); } |
constructor | Object |
Object.prototype | |
---|---|
[[Prototype]] | null |
constructor | Object |
toString | [native Funktion] |
toLocaleString | [native Funktion] |
valueOf | [native Funktion] |
hasOwnProperty | [native Funktion] |
isPrototypeOf | [native Funktion] |
propertyIsEnumerable | [native Funktion] |
Wenn wir maunzi.miau
notieren, dann wird nacheinander an diesen drei Objekten nach einer Eigenschaft mit diesem Namen gesucht.
Das Objekt maunzi
besitzt selbst keine Eigenschaften bis auf constructor
, welches auf die Funktion zeigt, mit dem es erzeugt wurde – das ist bekanntlich Katze
. Intern besitzt maunzi
einen Verweis auf seinen Prototyp, das ist Katze.prototype
. Dieser Verweis wird in der unsichtbaren Eigenschaft [[Prototype]]
gespeichert – lediglich in einigen JavaScript-Engines ist er über die Eigenschaft __proto__
les- und schreibbar.
Über diesen Verweis schreitet der JavaScript-Interpreter zum Prototypen von maunzi
, Katze.prototype
. Dort wird er fündig, denn dort existiert eine Eigenschaft namens miau
. Dies ist eine Funktion und sie kann mit dem Aufruf-Operator (…) ausgeführt werden.
Information
Auf diese Weise stehen alle Eigenschaften eines Prototypen, im Beispiel Katze.prototype
, auch beim Instanzobjekt zur Verfügung, im Beispiel maunzi
. Dies ist das ganze Geheimnis hinter der prototypischen Vererbung. Wenn bei einem Objekt selbst die angeforderten Eigenschaften nicht gefunden wurde, so dienen die Objekte in der Prototyp-Kette als Fallback. Man spricht von einer Delegation (Übertragung, Weitergabe). Das Objekt gibt die Anfrage an seine Prototypen weiter.
Prototypische Vererbung funktioniert grundlegend anders als klassenbasierte Vererbung, denn JavaScript ist eine äußerst dynamische Sprache. Es gibt keine Klassen, die einmal deklariert werden und nach der Kompilierung unveränderlich sind. Über die Prototyp-Kette erben gewöhnliche Objekte von gewöhnlichen Objekten. Jedes Objekt kann der Prototyp eines anderen Objektes werden und »einspringen«, wenn es die angeforderte Eigenschaft nicht selbst bereitstellen kann.
Alle beteiligten Objekte einschließlich der Prototypen können zur Laufzeit beliebig verändert werden. Ein Prototyp ist also im Gegensatz zu einer Klasse kein fester Bauplan für immer gleiche Instanzen, sondern selbst ein beliebiges Objekt, an das Eigenschaftsanfragen delegiert werden. Deshalb ist der Begriff »Instanz« für das Objekt, welches new Katze
erzeugt, letztlich irreführend. Wo es keine Klassen als rein abstrakten Bauplan gibt, sondern bloß flexible Objekte mittels Konstruktoren erzeugt werden und von Prototypen erben, so trifft dieser Begriff die Sache nicht. Er wird hier trotzdem der Einfachheit halber verwendet.
Prototypen verstehen: Die Glasplatten-Metapher[Bearbeiten]
Eine hervorragende Veranschaulichung von Prototypen hat Robin Debreuil ausgearbeitet. Er schrieb 2001 ein Tutorial über objektorientierte Programmierung mit ActionScript 1.0 in Flash 5.[1] ActionScript war damals eine Sprache, die auf ECMAScript 3 aufbaute und damit in den Grundzügen mit JavaScript identisch war. Die Sprache ActionScript ist mittlerweile von prototypenbasierter auf klassenbasierte Objektorientierung umgestiegen – für JavaScript sind diese Erklärungen aber immer noch gültig und aufschlussreich.
Die Metapher beschreibt Objekte in der Prototyp-Kette als beklebte Glasplatten. Auf jeder Glasplatte sind farbige Zettel an bestimmten Positionen aufgeklebt. Diese Zettel entsprechen den Objekteigenschaften, die Positionen entsprechen Eigenschaftsnamen. Die Instanz selbst verfügt über ein paar Zettel, sein Prototyp und dessen Prototyp über weitere. Diese können auch an denselben Stellen kleben.
Die Funktionalität, über die die Instanz verfügt, ist nun eine Summe der Zettel der drei Glasplatten: In der Metapher werden die Glasplatten übereinandergelegt. Da sie durchsichtig sind, schimmern durch die Lücken die Zettel auf den darunterliegenden Platten durch. Das Gesamtbild, das sich so ergibt, setzt sich aus den Zetteln der drei Platten zusammen.
Das Objekt ist demnach ein Mosaik, das sich aus den eigenen sowie fremden Eigenschaften zusammensetzt. Welches Objekt in der Prototyp-Kette nun eine gesuchte Eigenschaft bietet, ist unwichtig. An der gesuchten Stelle auf dem Glas ist ein Zettel sichtbar – in der Grafik z. B. oben links, dieser entspricht einem Eigenschaftsnamen, z. B. eins
.
Die Metapher zeigt auch, dass Objekte in der Prototyp-Kette gleiche Eigenschaften bieten können. An der Stelle oben in der Mitte klebt ein gelber Zettel auf der Instanz-Glasplatte, aber auch ein orangener, der die Konstruktor.prototype
darstellt. Beim Übereinanderlegen ist nur der gelbe Zettel der oben liegenden Instanz-Glasplatte sichtbar, der orangene wird überdeckt. Übertragen heißt das: Eine Eigenschaft kann am Instanzobjekt definiert werden, und schon überschreibt sie eine gleichnamige Eigenschaft am prototype-Objekt des Konstruktors.
Information
Diese Art der Vererbung nennt man Differential Inheritance. Im Gegensatz zur klassenbasierten Vererbung werden beim abgeleiteten Objekt (der Instanz) keine Eigenschaften erzeugt. Die Instanz ist keine Kopie des Prototypen, die Instanz kann sogar leer sein, wie es im Katzen-Beispiel der Fall ist. Erst wenn sich die Instanz vom Prototypen unterscheidet, wird bei der Instanz eine Eigenschaft angelegt, die einen anderen Wert als die des Prototypen besitzt. In der Glasplatten-Metapher bedeutet dies: An den Stellen, in denen die Instanz dem Prototyp gleicht, ist die Platte durchsichtig – sie delegiert. Wo sie sich unterscheidet, besitzt sie einen eigenen, andersfarbigen Zettel.
Nehmen wir an, dass hiesige Katzen meistens von der Rasse »Europäisch Kurzhaar« sind. Anstatt dies jedes Mal beim Erzeugen anzugeben, legen wir die Eigenschaft beim Prototypen an:
function Katze () {}
Katze.prototype.rasse = "Europäisch Kurzhaar";
var maunzi = new Katze();
alert(maunzi.rasse);
Greifen wir auf maunzi.rasse
zu, so wird die rasse
-Eigenschaft beim Prototypen gefunden. Denn die relevanten Objekte sehen so aus (Object.prototype
wurde ausgeblendet):
maunzi | |
---|---|
[[Prototype]] | Katze.prototype |
constructor | Katze |
Katze.prototype | |
---|---|
[[Prototype]] | Object.prototype |
rasse | "Europäisch Kurzhaar" |
constructor | Object |
Wenn wir nun eine Katze mit abweichender Rasse erzeugen wollen, so legen wir eine gleichnamige Eigenschaft bei der Instanz an, die die Eigenschaft des Prototypen verdeckt:
var maunzi = new Katze();
alert(maunzi.rasse); // Derzeit noch »Europäisch Kurzhaar« - vererbt vom Prototypen
maunzi.rasse = "Perser";
alert(maunzi.rasse); // Jetzt »Perser« - eigene Eigenschaft
Daraufhin besitzt die Instanz eine eigene, abweichende Eigenschaft:
maunzi | |
---|---|
[[Prototype]] | Katze.prototype |
rasse | "Perser" |
constructor | Katze |
Katze.prototype | |
---|---|
[[Prototype]] | Object.prototype |
rasse | "Europäisch Kurzhaar" |
constructor | Object |
Private Objekte anstatt private Eigenschaften[Bearbeiten]
ToDo (weitere ToDos)
- css-tricks: Implementing Private Variables In JavaScript von Khari McMillian on May 31, 2019 (Updated on Aug 28, 2021)
- https://dev.to/bhagatparwinder/classes-in-js-public-private-and-protected-1lok
Viele klassenbasierte Sprachen erlauben es, die Sichtbarkeit von Instanzeigenschaften festzulegen und unterscheiden beispielsweise zwischen öffentlichen und privaten Eigenschaften.
In JavaScript (ECMAScript 3) gibt es keine Möglichkeit, gewisse Eigenschaften eines Objektes als privat zu deklarieren, sodass sie ausschließlich in Methoden des Objektes zur Verfügung stehen. Sobald wir einem Objekt eine Eigenschaft hinzufügen, ist diese auch überall dort verfügbar, wo das Objekt verfügbar ist. Dagegen können Sie mit Object.freeze eine Eigenschaft als nicht überschreibbar und nicht löschbar deklarieren.
function Katze () {}
Katze.prototype.pfoten = 4;
Katze.prototype.miau = function () {
alert("Miau!");
};
var maunzi = new Katze();
var schnucki = new Katze();
// Überschreibe vom Prototyp vererbte Eigenschaft,
// indem eine gleichnamige Eigenschaft bei der Instanz erzeugt wird:
maunzi.pfoten = 5;
alert('Maunzi hat nun ' + maunzi.pfoten + ' Pfoten.');
// Überschreibe Methode des Prototyps:
Katze.prototype.miau = function () {
alert("Wau, wau!");
};
schnucki.miau();
Das obige Beispiel zeigt, wie ein Instanzobjekt und auch der Prototyp nachträglich verändert werden können: Plötzlich hat Maunzi fünf Pfoten und alle Katzen sagen »wau« anstatt »miau«. Dies ist sowohl ein Defizit von ECMAScript 3 als auch eine Stärke: Prototypen sind nicht abgeschlossen und Objekte immer erweiterbar.
Information
Wie gesagt gibt es keine privaten Eigenschaften im Wortsinn, auch wenn manche diesen Begriff auch auf JavaScript anwenden. Denn wenn ein Objekt an der Instanz hängt, ist es in ECMAScript 3 auch notwendig nach außen sichtbar und unkontrolliert überschreibbar. Wir können jedoch einen Trick anwenden, den wir bereits vom Revealing Module Pattern kennen: In einem Funktions-Scope notieren wir lokale Variablen und zudem die öffentlichen Methoden des Objektes. Die öffentlichen Methoden haben auf erstere Zugriff, weil sie im selben Scope erzeugt wurden und damit Closures sind. Daher handelt es sich um sogenannte privilegierte Methoden.
Da der Konstruktor bereits einen Funktions-Scope bereitstellt, nutzen wir diesen für private Objekte. Damit die Methoden auf die privaten Objekte Zugriff haben, müssen sie im Konstruktor erzeugt und dürfen nicht über den Prototyp definiert werden. Über this
werden sie ans Instanzobjekt gehängt.
function Katze (name) {
// --- Private Objekte
// Private Variablen
var pfoten = 4;
var gestreichelt = 0;
// name ist ebenfalls eine private Variable
// Private Funktionen
function miau () {
alert(name + ' macht miau!');
}
// --- Öffentliche (privilegierte) Eigenschaften
this.name = name;
// Öffentliche Methoden
this.kitzeln = function () {
alert(name + ' hat ' + pfoten + ' kitzlige Pfoten.');
miau();
};
this.streicheln = function () {
gestreichelt++;
miau();
};
}
var maunzi = new Katze('Maunzi');
maunzi.kitzeln();
maunzi.streicheln();
alert('maunzi.name: ' + maunzi.name);
// pfoten ist keine Objekt-Eigenschaft, also von außen unzugänglich:
alert('maunzi.pfoten: ' + maunzi.pfoten);
Der Konstruktor nimmt hier den Parameter name
entgegen. Dieser ist automatisch eine lokale Variable. Zusätzlich werden zwei lokalen Variablen (pfoten
, gestreichelt
) sowie eine lokale Funktion (miau
) angelegt. Der leeren Instanz werden eine Eigenschaft (name
) und zwei öffentliche Methoden (kitzeln
, streicheln
) angehängt, die als verschachtelte Funktionsausdrücke notiert werden.
Nach dem Anlegen einer Katze mit dem Namen Maunzi werden die beiden Methoden aufgerufen. Sie haben Lese- und Schreibzugriff auf die privaten Objekte und können auch die private Funktion ausführen, welche von außen nicht zugänglich sind.
Information
Wie das Beispiel zeigt, können auch private Funktionen angelegt werden. Private Funktionen können zum Beispiel interne, in verschiedenen Methoden verwendete Helfer sein. Sie entsprechen privaten Methoden in klassenbasierten Sprachen. Dieser Begriff ist auf JavaScript nicht anwendbar, da es sich eben nicht um Methoden des Instanzobjekt handelt. Sie sind lediglich in den tatsächlichen Instanzmethoden verfügbar, da diese Zugriff auf die Variablen des Konstruktor-Scopes haben.
Nachteile von privaten Objekten[Bearbeiten]
Der gravierende Unterschied zu den vorigen Beispielen ist, dass die Nutzung des Prototyps und damit die Vererbung wegfällt. Anstatt die Methoden einmal am Prototyp zu erzeugen, werden sie bei jeder Instanz im Konstruktor von neuem angelegt. Heraus kommt folgende Prototyp-Kette:
maunzi | |
---|---|
[[Prototype]] | Katze.prototype |
name | Maunzi |
kitzeln | function() {…} |
streicheln | function() {…} |
Katze.prototype | |
---|---|
[[Prototype]] | Object.prototype |
constructor | Object |
Object.prototype | |
---|---|
[[Prototype]] | null |
constructor | Object |
toString | [native Funktion] |
toLocaleString | [native Funktion] |
valueOf | [native Funktion] |
hasOwnProperty | [native Funktion] |
isPrototypeOf | [native Funktion] |
propertyIsEnumerable | [native Funktion] |
Der Prototyp ist demnach leer, die Eigenschaften hängen direkt an der Instanz und werden nicht vererbt. Dies hat verschiedene Konsequenzen:
Wenn Methoden über den Prototyp anstatt im Konstruktor erzeugt werden, wird Speicher eingespart. Dafür ist das Auflösen von Eigenschaften über die Prototyp-Kette etwas langsamer, da an mehreren Objekten gesucht wird.
Ein Überschreiben oder Löschen dieser Eigenschaften über den Prototyp ist nicht möglich. Ein nachträgliches Erweitern über den Prototyp ist möglich, diese Methoden sind allerdings nicht privilegiert, haben also keinen Zugriff auf private Objekte. Denn sie wurden nicht im Konstruktor definiert und schließen die privaten Objekte nicht ein. Aus demselben Grund ist kein nachträgliches Hinzufügen von privaten Objekten möglich.
Mit jedem Erzeugen einer Katze-Instanz werden alle Eigenschaften von neuem erzeugt. Die Instanzen teilen sich ihre Eigenschaften nicht mit anderen Katze-Instanzen. Dies wirkt sich negativ auf die Performance aus: Das Anlegen der Eigenschaften kostet Zeit und Arbeitsspeicher. Werden z. B. zehn Katzen instantiiert, so werden 20 Funktionsobjekte erzeugt (zehn mal kitzeln
und streicheln
). Würden die Instanzen diese Methoden vom Prototyp erben, so müssten sie nur einmal am Prototyp erzeugt werden.
Wozu Kapselung gut ist und wann sie nötig ist[Bearbeiten]
Kapselung in objektorientiertem JavaScript hat zwei Bedeutungen: Zum einen wird das Programm so strukturiert, dass es möglichst nicht mit anderen Scripten in Konflikt kommt. Dafür muss man in JavaScript selbst sorgen, da sich alle Scripte das globale window-Objekt teilen. Zum anderen bedeutet Kapselung die Trennung zwischen öffentlichen und privaten Objekten. Auf die öffentlichen können andere Scripte von außen zugreifen, die Privaten sind diesen verborgen.
Die erste Art der Kapselung ist in jedem Fall sinnvoll, eine effektive Kapselung der zweiten Art bringt jedoch Nachteile mit sich. Wann also brauchen wir sie und in welchem Umfang?
Das Konzept der Kapselung in der objektorientierten Programmierung hat den Zweck, zwischen einer öffentliche API und der internen Implementierung zu unterscheiden. Sie hilft in aller erster Linie dem Programmierer, gut strukturierte, wiederverwendbare Software zu schreiben.
Die API, also das Set an öffentlichen Methoden, wird dokumentiert und von anderen Programmierern angesteuert. Sie bleibt idealerweise über mehrere Programmversionen gleich. Sie kann natürlich erweitert und beizeiten durch eine leistungsfähigere und flexiblere Schnittstelle ergänzt werden.
Die privaten Objekte umfassen alle Variablen und Methoden, die nur intern Gebrauch finden und die tatsächliche Implementierung strukturieren. Ein bloßer Anwender des Scriptes muss diese nicht kennen. Von Version zu Version kann sich die interne Umsetzung bei gleichbleibender Funktionalität ändern, damit auch die privaten Objekte.
Ein zweiter Grund für Kapselung ist die effektive Sicherheit: Ein fremdes Script soll die Interna nicht lesen und manipulieren können. Dies ist in anderen Sprachen sehr wichtig. Manche JavaScript-Kenner argumentieren, dass der Aspekt der tatsächlichen Unsichtbarkeit für JavaScript nachrangig ist:
- Mit Tricks ist es bei einigen JavaScript-Interpretern möglich, auf einen privaten Funktions-Scope zuzugreifen.
- Es kann durchaus von Vorteil sein, die Interna eines Scriptes lesen, abändern und erweitern zu können. Das gehört zu den Features von JavaScript.
- Sie können einzelne Objekteigenschaften oder ganze Objekte mit Object.freeze »einfrieren« und vor Manipulation schützen.
Pseudo-private Objekte[Bearbeiten]
Aus diesen Gründen kann es ausreichen, Kapselung lediglich als Strukturierungskonzept zu verstehen. Es reicht dann, mit pseudo-privaten Objekten zu arbeiten. Diese sind nicht effektiv vor dem Zugriff von außen geschützt, finden jedoch in der Regel nur intern Verwendung.
Ein einfaches Object-Literal bietet keine effektive Kapselung, d.h. alle Eigenschaften sind von außen sichtbar und änderbar. Kapselung als Konzept lässt sich damit trotzdem umsetzen, indem klar zwischen öffentlichen und (pseudo-)privaten Eigenschaften unterschieden wird. Eine Konvention ist etwa, die privaten Eigenschaften mit einem _ (Unterstrich) beginnen zu lassen. Ein entsprechendes Modul könnte folgendermaßen aussehen:
var Modul = {
// Öffentliche Eigenschaften
öffentlicheMethode : function () {
alert(this._privateMethode());
},
// Pseudo-private Eigenschaften
_privateEigenschaft : 1;
_privateMethode : function () {
this._privateEigenschaft++;
return this._privateEigenschaft;
}
};
Modul.öffentlicheMethode();
Dasselbe bei einem Prototypen:
function Konstruktor () {}
// Öffentliche Eigenschaften
Konstruktor.prototype.öffentlicheMethode = function () {
alert(this._privateMethode());
};
// Pseudo-private Eigenschaften
Konstruktor.prototype._privateEigenschaft = 1;
Konstruktor.prototype._privateMethode = function () {
this._privateEigenschaft++;
return this._privateEigenschaft;
};
var instanz = new Konstruktor();
instanz.öffentlicheMethode();
Wie gesagt, technisch gesehen handelt es sich bei diesen pseudo-privaten Eigenschaften um ganz normale Eigenschaften, die sich in puncto Sichtbarkeit und Schreibbarkeit nicht von den sogenannten öffentlichen unterscheiden. Der Unterstrich im Namen hat keine Bedeutung für den JavaScript-Interpreter, er ist lediglich eine Namenskonvention.
Weblinks[Bearbeiten]
- ECMAScript 2015 (6th Edition, ECMA-262) Class Definitions
- MDN: Klassen
- ES6 compatibility table
- css-tricks: The Flavors of Object-Oriented Programming (in JavaScript)
- dmitrysoshnikov.com: ECMA-262-3 in detail. Chapter 7.1. OOP: The general theory.
- snook.ca: Why I don't love JavaScript's Module Pattern
Information
Quellen
- ↑ debreuil.com: Building Object-Oriented Programming
- ↑ Mathias Schäfer: Organisation von JavaScripten: Module und Kapselung