Objektorientierte Programmierung und Prototypische Vererbung in JavaScript
March 17, 2017
JavaScript unterscheidet sich grundlegend von klassenbasierten Sprachen wie Java oder C#. Es ist eine dynamische Sprache, die auf prototypischer Vererbung basiert. Obwohl ES6 Klassen eingeführt hat, sind diese unter der Haube lediglich "syntaktischer Zucker" über dem bestehenden Prototypen-System.
Dieser Leitfaden erklärt die Funktionsweise von Objekten, die verschiedenen Erstellungsmuster und das mächtige Prototypen-System von JavaScript im Detail.
1. Grundlagen: Objekte in JavaScript
Ein Objekt in JavaScript ist eine dynamische Sammlung von Schlüssel-Wert-Paaren (Key-Value-Pairs).
- Properties (Eigenschaften): Werte, die an einen Schlüssel gebunden sind.
- Methods (Methoden): Funktionen, die als Wert an einen Schlüssel gebunden sind und über das
this-Schlüsselwort auf den Zustand des Objekts zugreifen können.
const person = {
name: "Dave",
age: 27,
// Methode
greet() {
return `Hallo, ich bin ${this.name}.`;
}
};
Zugriffsmethoden: Punkt- vs. Klammernotation
- Punktnotation (
person.name): Die Standardmethode. Sie ist lesbar und schnell, erfordert jedoch, dass der Schlüssel ein gültiger JavaScript-Identifikator ist (keine Leerzeichen, startet nicht mit einer Zahl). - Klammernotation (
person["name"]): Erlaubt den Zugriff über Strings. Dies ist notwendig, wenn Schlüssel Sonderzeichen, Leerzeichen enthalten oder dynamisch zur Laufzeit ermittelt werden.
const person = {};
person.name = "John"; // Punktnotation
person["date of birth"] = "16. November"; // Klammernotation für ungültige Schlüssel
// Dynamischer Zugriff
const key = "name";
console.log(person[key]); // "John"
Iteration über Objekte
// Methode A: for...in Schleife (iteriert auch über vererbte Eigenschaften)
for (const key in person) {
if (person.hasOwnProperty(key)) { // Filtert vererbte Prototyp-Eigenschaften heraus
console.log(key, person[key]);
}
}
// Methode B: Object.keys() (empfohlen, gibt nur eigene Eigenschaften zurück)
Object.keys(person).forEach(key => {
console.log(key, person[key]);
});
2. Muster zur Objekterstellung
JavaScript bietet verschiedene Entwurfsmuster (Design Patterns) zur Erstellung von Objekten.
A. Objekt-Literale ({})
Der einfachste Weg, ein einzelnes Objekt zu erstellen.
const user = { name: "Alice" };
B. Factory-Funktionen
Eine Funktion, die ein neues Objekt erstellt und zurückgibt. Ideal, um die Objekterstellung zu kapseln, ohne das new-Schlüsselwort zu verwenden.
function createVehicle(year, make) {
return {
year,
make,
getInfo() {
return `${this.make} (${this.year})`;
}
};
}
const car = createVehicle(2018, "Toyota");
C. Konstruktor-Funktionen
Klassischer Weg vor ES6. Konstruktoren werden per Konvention mit einem Großbuchstaben (PascalCase) benannt und müssen zwingend mit dem new-Schlüsselwort aufgerufen werden.
function Vehicle(year, make) {
this.year = year;
this.make = make;
}
// Das 'new'-Keyword erstellt ein neues leeres Objekt,
// bindet 'this' an dieses Objekt und gibt es zurück.
const car = new Vehicle(2016, "Honda");
D. Object.create()
Erstellt ein neues Objekt und setzt dessen Prototyp direkt auf ein bestehendes Objekt.
const animalProto = {
makeSound() { console.log(this.sound); }
};
const dog = Object.create(animalProto);
dog.sound = "Wau!";
dog.makeSound(); // "Wau!"
3. Kapselung in Konstruktoren: Öffentlich vs. Privat
In Konstruktor-Funktionen können Sie das Konzept von Kapselung (Encapsulation) und Datenschutz simulieren, indem Sie Variablen als lokale Variablen deklarieren, anstatt sie an this zu binden. Diese Variablen sind dann nur über Closures (Methoden innerhalb des Konstruktors) zugänglich.
function BankAccount(initialBalance) {
// Private Variable (nur über die Closure zugänglich)
let balance = initialBalance;
// Öffentliche Methode (kann auf 'balance' zugreifen)
this.getBalance = function() {
return balance;
};
this.deposit = function(amount) {
if (amount > 0) {
balance += amount;
}
};
}
const account = new BankAccount(1000);
console.log(account.balance); // undefined (privat!)
console.log(account.getBalance()); // 1000
4. Prototypische Vererbung: Das Herz von JavaScript
Jedes Objekt in JavaScript besitzt eine interne Verbindung zu einem anderen Objekt, dem Prototyp (referenziert über __proto__ oder abrufbar über Object.getPrototypeOf()). Wenn Sie auf eine Eigenschaft oder Methode zugreifen, die nicht direkt auf dem Objekt existiert, sucht JavaScript in dessen Prototyp danach. Dieser Vorgang setzt sich fort, bis das Ende der Prototypen-Kette (Prototype Chain) erreicht ist (Object.prototype hat den Prototyp null).
Die Prototypen-Kette aufbauen
const grandGrand = { hobbies: ["Musik"], isAwesome: true };
const father = { age: 27 };
const son = { firstName: "Toby" };
// Prototypen-Kette manuell aufbauen (ES6)
Object.setPrototypeOf(son, father);
Object.setPrototypeOf(father, grandGrand);
// Der Sohn erbt alle Eigenschaften seiner Vorfahren
console.log(son.isAwesome); // true (von grandGrand vererbt)
console.log(son.hobbies); // ["Musik"] (von grandGrand vererbt)
Verschiedene Wege zur Prototyp-Zuweisung
const animal = { kind: "Säugetier" };
// 1. ES6 __proto__ (Veraltet, nicht in produktivem Code verwenden)
const dog = {};
dog.__proto__ = animal;
// 2. Object.setPrototypeOf (Standard-Weg zur nachträglichen Änderung)
const cat = {};
Object.setPrototypeOf(cat, animal);
// 3. Object.create (Der sauberste Weg beim Erstellen)
const horse = Object.create(animal);
5. Konstruktor-Methoden vs. Prototyp-Methoden (Performance)
Ein entscheidender Aspekt für die Performance und den Speicherbedarf einer Anwendung ist die Frage, wo Methoden definiert werden: im Konstruktor selbst oder auf dessen Prototyp.
Methode im Konstruktor (Speicherintensiv)
function Person(name) {
this.name = name;
this.greet = function() {
console.log(`Hallo, ${this.name}`);
};
}
Problem: Bei jeder Instanziierung (new Person()) wird die greet-Funktion neu im Speicher erstellt. Bei 10.000 Instanzen existieren 10.000 identische Kopien der Funktion im Speicher.
Methode im Prototyp (Speichereffizient)
function Person(name) {
this.name = name;
}
// Die Methode wird auf dem gemeinsamen Prototyp-Objekt definiert
Person.prototype.greet = function() {
console.log(`Hallo, ${this.name}`);
};
Vorteil: Die Methode greet existiert nur ein einziges Mal im Speicher. Alle Instanzen von Person zeigen auf denselben Prototyp und teilen sich diese Methode. Dies spart signifikant Arbeitsspeicher.
Dynamische Updates von Prototypen
Ein weiterer Vorteil von Prototypen ist, dass Änderungen daran sofort für alle existierenden Instanzen wirksam werden.
const john = new Person("John");
// Wir fügen dem Prototyp nachträglich eine Methode hinzu
Person.prototype.sayGoodbye = function() {
console.log(`Tschüss, ${this.name}`);
};
// Die bereits existierende Instanz kann die neue Methode sofort nutzen!
john.sayGoodbye(); // "Tschüss, John"
6. JSON-Serialisierung
Objekte können mit JSON (JavaScript Object Notation) leicht in Strings umgewandelt und wieder zurückkonvertiert werden. Dies ist die Grundlage für den Datenaustausch mit Web-APIs.
const user = { name: "Alice", role: "Admin" };
// Objekt in JSON-String konvertieren (Serialisierung)
const jsonString = JSON.stringify(user); // '{"name":"Alice","role":"Admin"}'
// JSON-String in JavaScript-Objekt konvertieren (Deserialisierung)
const userObj = JSON.parse(jsonString);
Hinweis: Bei der Serialisierung zu JSON gehen Methoden (Funktionen) des Objekts verloren, da JSON nur reine Datenstrukturen unterstützt.
Fazit
Das Prototypen-System ist das fundamentale Fundament von JavaScript. Während Konstruktoren und Klassen eine vertraute objektorientierte Syntax bieten, ist das Verständnis der prototypischen Vererbung entscheidend, um speichereffizienten und performanten Code zu schreiben. Die Definition von Methoden auf dem Prototyp statt im Konstruktor ist eine grundlegende Best Practice zur Optimierung des Speicherbedarfs Ihrer Anwendung.