Zum Inhalt
wannacode

Vererbung

Kurz gesagt: Vererbung bedeutet, dass eine Klasse (die Unterklasse) die Felder und Methoden einer anderen Klasse (der Basisklasse) bekommt, sodass das Gemeinsame an einer Stelle steht.

Problem

Zwei Kopien desselben Codes

Das Gehaltsprogramm hat eine Klasse Employee mit Name, Gehalt und einer Methode für eine Erhöhung. Dann brauchtest du auch einen Manager, der zusätzlich einen Bonus bekommt. Am schnellsten ging es, Employee in eine neue Klasse Manager zu kopieren und den Bonus hinzuzufügen.

Einen Monat später gibt jemand eine Erhöhung von -10% statt 10% ein. Also fügst du Employee eine Prüfung hinzu. In Manager vergisst du sie:

// in Employee
raise(percent) {
if (percent > 0) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
}
// in Manager: eine Kopie von vor der Prüfung
raise(percent) {
this.#salary += Math.trunc(this.#salary * percent / 100);
}
const ivan = new Employee("Ivan", 2000);
const maria = new Manager("Maria", 3000, 500);
ivan.raise(-10);
maria.raise(-10);
console.log(String(ivan));
console.log(String(maria));
Ausgabe
Ivan: 2000 €
Maria: 2700 €

Das Gehalt von Ivan ist geschützt, aber das von Maria ist um 300 € gefallen. Die zwei Klassen sind zu 90% gleich, und trotzdem muss jede Korrektur zweimal gemacht werden.

Die Idee

Das Gemeinsame kommt in die Basisklasse

Ein Manager ist ein Mitarbeiter, nur mit etwas zusätzlich. Sag es im Code: class Manager extends Employee. In Manager schreibst du nur, was neu ist, den Bonus:

class Manager extends Employee {
#bonus;
constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
get bonus() {
return this.#bonus;
}
}
const maria = new Manager("Maria", 3000, 500);
maria.raise(-10);
maria.raise(10);
console.log(String(maria));
console.log(`Bonus: ${maria.bonus} €`);
Ausgabe
Maria: 3300 €
Bonus: 500 €

Manager hat jetzt alles, was Employee hat: einen Namen, ein Gehalt, raise() mit der Prüfung, toString(). Deshalb ist die Erhöhung um -10% nicht durchgegangen. Die Prüfung steht einmal da und funktioniert für beide Klassen.

Das nennt man Vererbung. Employee ist die Basisklasse (Elternklasse), und Manager ist die Unterklasse. Die Unterklasse nimmt alles von der Basisklasse und fügt ihr Eigenes hinzu.

Ob Vererbung sinnvoll ist, prüfst du mit dem Satz „X ist ein Y“. „Ein Manager ist ein Mitarbeiter“ stimmt. „Eine Rechnung ist ein Mitarbeiter“ stimmt nicht, also erbt die Rechnung nicht von Employee, auch wenn sie einen Betrag und einen Namen hat.

Probier es aus

Was bekommt die Praktikantin?

Intern hat nichts Eigenes: Der Rumpf ist leer, {}.

Was gibt das Programm aus?

class Intern extends Employee {}
const ana = new Intern("Ana", 1200);
ana.raise(5);
console.log(String(ana));
Antwort

Die Idee

Die Basisklasse kommt zuerst

#name und #salary sind in Employee versteckt. Die Unterklasse hat sie, sieht sie aber nicht: Für sie sind sie versteckt, wie für jede andere Klasse. Wie gibt Manager ihnen dann einen Wert?

Indem er den Konstruktor der Basisklasse aufruft:

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}

super(name, salary) ruft den Konstruktor von Employee mit diesen Argumenten auf. Er richtet #name und #salary ein, und dann richtet Manager #bonus ein. In JavaScript muss super(...) aufgerufen werden, bevor der Konstruktor this anfasst. Wenn du es vergisst, bricht new Manager(...) mit ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor ab.

Es gibt ein Objekt. Es enthält den Teil der Basisklasse und den Teil der Unterklasse, und jeder Teil wird von seinem eigenen Konstruktor eingerichtet.

constructor(name, salary, bonus) {
super(name, salary);
this.#bonus = bonus;
}
const maria = new Manager("Maria", 3000, 500);
console.log(`${maria}, Bonus ${maria.bonus} €`);
Schritt für Schritt, in Worten
  1. new erzeugt ein Objekt. Es wird den Teil von Employee und den Teil von Manager enthalten.
  2. Der Konstruktor von Manager startet mit den drei Argumenten.
  3. Die Basisklasse kommt zuerst: super(name, salary) ruft den Konstruktor von Employee auf. Davor darfst du this nicht anfassen.
  4. Der Konstruktor von Employee richtet seinen Teil ein: #name und #salary.
  5. Erst dann richtet der Rumpf von Manager den Bonus ein.
  6. Es gibt ein Objekt, und jeder Teil wird von seinem eigenen Konstruktor eingerichtet.
Genauer, für Neugierige
  • Der Konstruktor wird vererbt. Deshalb nimmt Intern ohne eigenen constructor Name und Gehalt von Employee: JavaScript ruft die Basisklasse für dich mit den übergebenen Argumenten auf.
  • Sobald du einen eigenen constructor schreibst, ist super(...) deine Aufgabe. Die Felder der Unterklasse (#bonus) werden erst angelegt, nachdem super(...) fertig ist.

Probier es aus

Welcher Konstruktor läuft zuerst?

Hier ruft die Unterklasse die Basisklasse nicht ausdrücklich auf:

Was gibt das Programm aus?

class Account {
constructor() {
console.log("Das Konto ist eröffnet");
}
}
class SavingsAccount extends Account {
constructor() {
console.log("Zinsen: 3%");
}
}
const savings = new SavingsAccount();
Antwort

Probier es aus

Finde den Fehler

Maria soll ein Gehalt von 3000 € und einen Bonus von 500 € haben. Aber das Programm gibt aus:

Ausgabe
Maria: 500 €

In welcher Zeile ist der Fehler?

Wähle die Zeile mit dem Fehler.

Die Idee

Kein protected: ein Unterstrich

Am Jahresende wird der Bonus des Managers zum Gehalt addiert. Die Methode steht in Manager, aber das Gehalt ist #salary in Employee. Der Getter liest es nur, und hier musst du es ändern. Und this.#salary ist in Manager nicht erlaubt: Das Programm startet gar nicht erst, SyntaxError: Private field '#salary' must be declared in an enclosing class.

In Java und C# gibt es dafür eine dritte Zugriffsstufe, protected: Die Klasse und ihre Unterklassen sehen es, alle anderen nicht. JavaScript hat das nicht. Stattdessen schreibst du das Feld mit einem Unterstrich, _salary: eine Abmachung für „intern, für die Klasse und ihre Unterklassen“, die die Sprache nicht prüft:

class Employee {
#name;
constructor(name, salary) {
this.#name = name;
this._salary = salary;
}
// ... der Rest wie vorher, mit this._salary
}
class Manager extends Employee {
// ... #bonus und der Konstruktor
mergeBonus() {
this._salary += this.#bonus;
this.#bonus = 0;
}
}
const maria = new Manager("Maria", 3000, 500);
maria.mergeBonus();
console.log(String(maria));
console.log(`Bonus: ${maria.bonus} €`);
Ausgabe
Maria: 3500 €
Bonus: 0 €

Die Abmachung ist kein Schutz: maria._salary = 0 im Programm geht auch durch, ohne Fehler. Deshalb bleibt alles, was die Unterklasse nicht anfassen soll, bei # (#name), und nur das Feld, das sie wirklich ändern muss, bekommt einen Unterstrich.

Genauer, für Neugierige
  • # ist echter Schutz: Das Feld ist weder von außen noch aus den Unterklassen erreichbar. Deshalb wird auch geprüft, bevor das Programm startet, nicht während es läuft.

Probier es aus

Was fehlt?

Eine Praktikantin ist eine Mitarbeiterin. Was steht in der Lücke?

class Intern  Employee {}

Die Idee

Eine Basisklasse und ein gemeinsamer Vorfahr

Eine Klasse hat genau eine Basisklasse. Sie kann nicht von Employee und Student zugleich erben. Deshalb ist die Vererbung einfach.

Die Kette kann lang sein. Intern erbt von Employee. Und wovon erbt Employee, wenn es kein extends hat? Jede Klasse ohne Basisklasse erbt von Object. Das ist die gemeinsame Basisklasse aller Klassen. Deshalb hat sogar eine leere Klasse Methoden:

class Ticket {}
const ticket = new Ticket();
console.log(ticket.toString());
console.log(ticket);
Ausgabe
[object Object]
Ticket {}

toString() kommt von Object und gibt [object Object] zurück. Im Modul „Klassen und Objekte“ hast du ein eigenes toString() geschrieben. Jetzt weißt du, dass es die Version von Object ersetzt. Mehr dazu in der nächsten Lektion.

console.log(ticket) ruft toString() nicht auf. Es zeigt die Klasse und die Felder: Ticket {}. Deshalb geben wir in dieser Lektion String(maria) aus: String() ruft toString() auf.

Genauer, für Neugierige
  • In JavaScript gibt es keine Klasse, von der man nicht erben kann.
  • class WorkingStudent extends Employee, Student startet nicht: SyntaxError. Nach extends steht genau eine Klasse.

Übung

Unterklassen

Löse die Aufgaben in deinem Editor. Die Basisklassen sind aus dem Modul „Klassen und Objekte“. Wenn du sie nicht zur Hand hast, schreib sie neu: Sie sind kurz.

1. Familienauto. SimpleCar hat eine Geschwindigkeit und eine Methode accelerate(), die sie um 10 erhöht. Schreib FamilyCar, das von SimpleCar erbt, mit 4 Sitzen beginnt und eine Methode addSeat() hat. Erzeuge ein Familienauto, beschleunige es und füge einen Sitz hinzu:

Ausgabe
Geschwindigkeit: 10 km/h
Sitze: 5

2. Studentin. Person hat Name, Alter und introduce(). Schreib Student, das von Person erbt und einen Notendurchschnitt grade (eine Dezimalzahl) hinzufügt. Benutze den Konstruktor der Basisklasse.

Ausgabe
Ich bin Elena, 19 Jahre alt.
Notendurchschnitt: 5.5

3. Breakdance. DanceMove hat einen Namen, eine Dauer in Sekunden und perform(). Schreib BreakDanceMove mit einer Schwierigkeit von 1 bis 10 und einer Methode showComplexity().

Ausgabe
Ich tanze moonwalk für 5 Sekunden
Ich tanze windmill für 12 Sekunden
windmill: Schwierigkeit 8 von 10

4. Elektroauto. Car hat eine Marke, Kraftstoff in Litern und die gefahrenen Kilometer. refuel(liters) tankt nach, und drive(distance) verbraucht 1 Liter auf 10 km und addiert die Kilometer (reicht der Kraftstoff nicht, sagt die Methode es und fährt nicht). Schreib ElectricCar:

  • ohne Kraftstoff und mit einem Akku, der bei 100% beginnt;
  • mit einer Effizienz: wie viele km es mit 1% Akku fährt (5 beim „Tesla“);
  • charge() lädt den Akku auf 100%;
  • driveElectric(distance) verbraucht Akku statt Kraftstoff und addiert ebenfalls die Kilometer.
Wenn die Kilometer in Car #mileage sind, kann die Unterklasse sie nicht ändern. Tipp: die Karte über den Unterstrich.
Ausgabe
Skoda: 150 km, Kraftstoff 35 l
Tesla: 200 km, Akku 60%
Der Akku reicht nicht für 400 km
Tesla: 600 km, Akku 20%

Weitere Aufgaben

  • Arbeiter. Worker erbt von Person und fügt eine Berufsbezeichnung und eine Methode work() hinzu:
Ausgabe
Ich bin Peter, 34 Jahre alt.
Ich arbeite als Buchhalter
  • Praktikantin mit Mentor. Füge Intern ein Feld mit dem Namen des Mentors hinzu und übergib ihn im Konstruktor, zusammen mit Name und Gehalt für die Basisklasse. Jetzt braucht Intern doch ein eigenes constructor.

Test

Teste dich

1. class Manager extends Employee. Was stimmt?
2. Das Feld #name steht in Employee. Kann Manager in einer seiner Methoden this.#name = "Maria" schreiben?
3. Was macht super(name, salary) im Konstruktor von Manager?
4. Wie viele Basisklassen kann eine Klasse haben?
5. JavaScript hat kein protected. Wie schreibst du ein Feld, das die Unterklassen benutzen und alle anderen in Ruhe lassen sollen?

Erstellt von Ivo Gergov im Rahmen der Kampagne von Evo Tech für zugängliche Programmierausbildung ·