Zum Inhalt
wannacode

Klassen und Daten in Klassen

Kurz gesagt: Eine verschachtelte Klasse ist eine Klasse in einer anderen Klasse, weil sie nur dort Sinn ergibt; in JavaScript ist sie ein statisches Feld, und Daten werden als Objekte und Arrays ineinander verschachtelt.

Problem

Eine Klasse, die nur an einer Stelle Sinn ergibt

In der Lektion über die verkettete Liste war Node eine eigene Klasse. Jeder im Programm kann Knoten anlegen, aber sie ergeben nur in der Liste Sinn. Genauso ist es mit einer Zeile der Bestellung:

class OrderLine {
constructor(product, quantity, price) {
this.product = product;
this.quantity = quantity;
this.price = price;
}
}
class Order {
lines = [];
}

OrderLine ohne Order ergibt keinen Sinn. Der Name ist lang, damit er nicht mit der Zeile einer Rechnung, InvoiceLine, oder mit der Zeile im Lager zusammenstößt. Und nichts sagt, dass die beiden Klassen zusammengehören. Kann die Zeile innerhalb der Bestellung leben?

Die Idee

Verschachtelte Klasse

Ja. Eine Klassendeklaration in einer Klasse gibt es in JavaScript nicht, aber eine Klasse ist ein Wert und kann ein statisches Feld sein. So heißt sie von außen Order.Line, wie eine verschachtelte Klasse (nested class) in Java:

class Order {
static Line = class {
constructor(product, quantity, price) {
this.product = product;
this.quantity = quantity;
this.price = price;
}
total() {
return this.quantity * this.price;
}
};
#lines = [];
add(line) {
this.#lines.push(line);
}
total() {
let sum = 0;
for (const line of this.#lines) {
sum += line.total();
}
return sum;
}
}
const order = new Order();
order.add(new Order.Line("Kaffeebohnen", 2, 12.5));
order.add(new Order.Line("Tasse", 4, 3.0));
console.log(`Gesamt: ${order.total().toFixed(2)} €`);
Ausgabe
Gesamt: 37.00 €
  • static Line = class { ... } ist ein statisches Feld von Order, dessen Wert eine Klasse ohne Namen ist. Von außen und innerhalb der Methoden heißt sie Order.Line.
  • new Order.Line(...) entsteht ohne ein Order-Objekt, wie bei einer normalen Klasse.
  • Order.Line ist an keine bestimmte Bestellung gebunden und sieht deren Felder nicht. In Java heißt so eine Klasse statisch.

Diese Schreibweise siehst du seltener. Oft steht die kleine Klasse einfach neben der großen, in derselben Datei, oder statt einer Klasse gibt es ein Objekt mit Feldern. Darum geht es in der nächsten Karte.

Die Idee

Daten in Daten: Objekte und Arrays

Hält die Zeile der Bestellung nur Werte und hat keine Methoden, gibt es in JavaScript oft keine Klasse dafür. Die Bestellung ist ein Objekt, in dem ein Array von Objekten steht:

const order = {
customer: "Schmidt",
lines: [
{ product: "Kaffeebohnen", quantity: 2, price: 12.5 },
{ product: "Tasse", quantity: 4, price: 3.0 },
],
};
let total = 0;
for (const line of order.lines) {
total += line.quantity * line.price;
}
console.log(order.lines[0].product);
console.log(`Gesamt: ${total.toFixed(2)} €`);
Ausgabe
Kaffeebohnen
Gesamt: 37.00 €
  • order.lines ist das Array, order.lines[0] ist die erste Zeile, und order.lines[0].product ist das Produkt darin. Jeder Punkt und jedes [...] geht eine Ebene tiefer.
  • Hier ist das Objekt für bekannte Felder (customer, lines). Für Schlüssel, die erst zur Laufzeit kommen, gibt es Map, wie in der Lektion über Maps.
  • Eine Klasse ist die bessere Wahl, wenn die Daten Verhalten haben (eine Methode total) oder Regeln, die eingehalten werden müssen.

Eine Map von Arrays sammelt Dinge in Gruppen:

const people = [["Jonas", "Berlin"], ["Maria", "Hamburg"], ["Anna", "Berlin"]];
const byCity = new Map();
for (const [name, city] of people) {
if (!byCity.has(city)) {
byCity.set(city, []);
}
byCity.get(city).push(name);
}
console.log(byCity);
console.log(byCity.get("Berlin")[1]);
Ausgabe
Map(2) { 'Berlin' => [ 'Jonas', 'Anna' ], 'Hamburg' => [ 'Maria' ] }
Anna
Genauer, für Neugierige
  • Eine Falle bei verschachtelten Arrays: fill legt ein und dieselbe Referenz an alle Stellen. Die drei Zeilen sind ein und dasselbe Array:
let grid = Array(3).fill([0, 0, 0]);
grid[0][0] = 1;
console.log(grid);
grid = [];
for (let row = 0; row < 3; row++) {
grid.push([0, 0, 0]);
}
grid[0][0] = 1;
console.log(grid);
Ausgabe
[ [ 1, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ]
[ [ 1, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]
  • console.log zeigt nur ein paar Ebenen. Weiter unten schreibt es [Array] oder [Object]. Das ganze Objekt zeigt JSON.stringify:
const shop = { name: "Café", menu: { drinks: { hot: ["Kaffee", "Tee"] } } };
console.log(shop);
console.log(JSON.stringify(shop));
Ausgabe
{ name: 'Café', menu: { drinks: { hot: [Array] } } }
{"name":"Café","menu":{"drinks":{"hot":["Kaffee","Tee"]}}}

Die Idee

Das verschachtelte Objekt hält das äußere

Der Warenkorb hat einen Rabatt für alle Produkte darin. Jedes Produkt muss den Rabatt seines eigenen Warenkorbs kennen. In JavaScript sieht eine verschachtelte Klasse das äußere Objekt nicht, darum wird der Warenkorb ausdrücklich übergeben:

class Cart {
static Item = class {
constructor(cart, name, price) {
this.cart = cart;
this.name = name;
this.price = price;
}
finalPrice() {
return (this.price * (100 - this.cart.#discountPercent)) / 100;
}
};
#discountPercent;
#items = [];
constructor(discountPercent) {
this.#discountPercent = discountPercent;
}
add(name, price) {
this.#items.push(new Cart.Item(this, name, price));
}
print() {
for (const item of this.#items) {
console.log(`${item.name}: ${item.finalPrice().toFixed(2)} €`);
}
}
}
const cart = new Cart(10);
cart.add("Kopfhörer", 80);
cart.add("Hülle", 20);
cart.print();
Ausgabe
Kopfhörer: 72.00 €
Hülle: 18.00 €
Java hat eine innere Klasse (inner class): Jedes Objekt davon hält eine versteckte Referenz auf das äußere Objekt. JavaScript hat so etwas nicht, darum ist die Referenz sichtbar: add übergibt this, und Item hält sie in this.cart. finalPrice liest den #discountPercent des Warenkorbs, in dem es liegt. Item ist innerhalb von Cart geschrieben, darum sieht es die #-Felder des Warenkorbs, so wie eine innere Klasse in Java die private sieht.

const cart = new Cart(10);
cart.add("Kopfhörer", 80);
cart.add("Hülle", 20);
cart.print();
Schritt für Schritt, in Worten
  1. Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
  2. add erzeugt ein Item und übergibt ihm den Warenkorb, this. Neben name und price hält jedes Item in cart einen Pfeil zu seinem Warenkorb.
  3. Das zweite Item zeigt auf denselben Warenkorb.
  4. finalPrice von „Kopfhörer“ folgt dem Pfeil this.cart und liest #discountPercent: 80 * (100 - 10) / 100 = 72.
  5. finalPrice von „Hülle“ folgt dem Pfeil this.cart und liest #discountPercent: 20 * (100 - 10) / 100 = 18.
Genauer, für Neugierige
  • Von außen wird ein Item-Objekt mit dem Warenkorb als erstem Argument erzeugt. Meist erzeugt die äußere Klasse die inneren selbst, wie add hier:
const cart = new Cart(10);
const cable = new Cart.Item(cart, "Kabel", 10);
console.log(cable.finalPrice());
Ausgabe
9
  • Das Feld cart ist genau das, was in Java das versteckte Cart.this macht. Hier siehst du es einfach.
  • Benutzt die verschachtelte Klasse die Felder der äußeren nicht, gib ihr das äußere Objekt nicht. So hält sie keine Referenz, die sie nicht braucht.

Probier es aus

Zwei Warenkörbe

Die Klasse Cart ist dieselbe. Ein Warenkorb hat keinen Rabatt, der andere 20 % für Clubmitglieder.

Was gibt das Programm aus?

const regular = new Cart(0);
const member = new Cart(20);
regular.add("Buch", 15);
member.add("Buch", 15);
regular.print();
member.print();
Antwort

Probier es aus

Finde den Fehler

Die Zeile soll die Währung der Bestellung zeigen. Bei console.log(new Order.Line("Kaffee", 12.5).show()) bricht das Programm ab:
Fehler
ReferenceError: currency is not defined

In welcher Zeile liegt das Problem?

Wähle die Zeile mit dem Fehler.

Die Idee

Eine Klasse in einer Funktion

Eine Klasse kann man auch in einer Funktion deklarieren. Dann ist sie nur dort sichtbar und heißt lokale Klasse:

function printBestSeller(names, sold) {
class Sale {
constructor(name, count) {
this.name = name;
this.count = count;
}
}
let best = new Sale(names[0], sold[0]);
for (let i = 1; i < names.length; i++) {
if (sold[i] > best.count) {
best = new Sale(names[i], sold[i]);
}
}
console.log(`Bestseller: ${best.name} (${best.count} Stk.)`);
}
const names = ["Kaffee", "Tee", "Wasser"];
const sold = [120, 45, 300];
printBestSeller(names, sold);
Ausgabe
Bestseller: Wasser (300 Stk.)

Sale hält den Namen und die Anzahl zusammen. Außerhalb von printBestSeller benutzt sie niemand, darum steht sie dort. Solche Klassen sind selten, aber es ist gut, sie zu kennen.

Die Idee

Ein Objekt statt einer Klasse

Ein Rabatt im Shop ist etwas mit einer Methode apply(price). Im Modul „Vererbung und Polymorphie“ hast du gesehen, dass JavaScript kein interface hat: Das Objekt muss nur die Methoden haben. Du könntest für jeden Rabatt eine Klasse machen: class BlackFriday { apply(price) { ... } }. In Java gibt es für einen Rabatt, der nur an einer Stelle benutzt wird, die anonyme Klasse: eine Klasse ohne Namen, genau dort geschrieben. In JavaScript brauchst du meist nicht einmal eine Klasse. Du schreibst das Objekt mit der Methode direkt hin:

const blackFriday = {
apply(price) {
return (price * 70) / 100;
},
};
const none = {
apply(price) {
return price;
},
};
console.log(blackFriday.apply(100));
console.log(none.apply(100));
Ausgabe
70
100
  • { apply(price) { ... } } ist ein Objekt mit einer Methode. Eine Klasse gibt es nicht, und blackFriday.apply(100) funktioniert wie mit einem Objekt einer Klasse.
  • JavaScript gibt 70 aus, nicht 70.0: Die Zahl ist ganz.
Genauer, für Neugierige
  • Eine Klasse ohne Namen gibt es in JavaScript doch: class { ... } ist ein Ausdruck und kann sofort erzeugt werden. Man schreibt das selten, weil das Objekt mit einer Methode kürzer ist:
const none = new (class {
apply(price) {
return price;
}
})();
console.log(none.apply(100));
console.log(none.constructor.name === "");
Ausgabe
100
true
  • Ist der Rabatt nur eine Aktion, übergibt man in JavaScript oft direkt eine Funktion, wie die Vergleichsfunktion in der nächsten Karte. Im Modul „Modernes JavaScript“ siehst du eine kürzere Schreibweise für kleine Funktionen.

Die Idee

Vergleichsfunktion: Sortieren nach eigener Regel

sort hat Text sortiert, und Zahlen mit einer Funktion. Mit Schülern und ohne Funktion gibt es keinen Fehler, sortiert aber nach dem Text aus toString:

class Student {
constructor(name, grade) {
this.name = name;
this.grade = grade;
}
toString() {
return `${this.name} ${this.grade}`;
}
}
const students = [
new Student("Jonas", 5.50),
new Student("Maria", 6.00),
new Student("Peter", 4.75),
new Student("Anna", 5.50),
];
students.sort();
console.log(students.join(", "));
Ausgabe
Anna 5.5, Jonas 5.5, Maria 6, Peter 4.75

Hier kommen sie zufällig nach Namen heraus, weil toString mit dem Namen beginnt. Ohne toString wäre jeder Schüler der Text [object Object].

Die Regel gibst du als Vergleichsfunktion an, wie bei den Zahlen in der Lektion über Arrays. Sie bekommt zwei Schüler a und b und gibt eine negative Zahl zurück, wenn a vor b kommen soll, null, wenn beide gleich sind, und eine positive Zahl, wenn a nach b kommt:

students.sort(function (a, b) {
return b.grade - a.grade;
});
console.log(students.join(", "));
Ausgabe
Maria 6, Jonas 5.5, Anna 5.5, Peter 4.75
  • b.grade - a.grade vergleicht andersherum, darum steht die höchste Note vorn.
  • Jonas und Anna haben dieselbe Note und bleiben in der Reihenfolge, in der sie waren. sort vertauscht gleiche Elemente nicht.
  • join ruft toString für jeden Schüler auf.
  • Für Text in umgekehrter Reihenfolge: sort() und danach reverse().
const cities = ["Hamburg", "Berlin", "München"];
cities.sort();
cities.reverse();
console.log(cities);
Ausgabe
[ 'München', 'Hamburg', 'Berlin' ]
Genauer, für Neugierige
  • sort sortiert das Array selbst. Brauchst du ein neues, gibt es toSorted mit derselben Funktion.
  • b.grade - a.grade ist sicher, weil Zahlen in JavaScript nicht überlaufen wie int in Java. Für Text gibt es keine Subtraktion: Dort nimmst du a.name.localeCompare(b.name) aus der Lektion über Text.

Probier es aus

Was steht vorn?

Was gibt das Programm aus?

const products = [
new Product("Monitor", 220),
new Product("Maus", 25),
new Product("Tastatur", 60),
];
products.sort(function (a, b) {
return a.price - b.price;
});
for (const p of products) {
console.log(p.name);
}
Antwort

Übung

Klassen, die zusammengehören

Löse die Aufgaben in deinem Editor.

1. Warenkorb. Eine Klasse ShoppingCart mit einer verschachtelten Klasse Item (Name und Preis, static Item = class { ... }) und den Methoden addItem(item), getTotal() und count(). Füge den Artikel new ShoppingCart.Item("Milch", 2.5) hinzu, dazu Brot für 1,20 € und Kaffee für 7,50 €. Gib die Summe mit toFixed(2) aus.

Ausgabe
Artikel: 3
Gesamt: 11.20 €

2. Monitor für einen Webserver. Eine Klasse WebServer mit einer verschachtelten Klasse Request (Adresse und Statuscode) und den Methoden addRequest(r) und printStats(). Die Statistik sagt, wie viele Anfragen es gibt, wie viele es von jedem Statuscode gibt (als Zahlen sortiert) und wie viel Prozent erfolgreich sind, also mit einem Code von 200 bis 299. Halte die Zähler in einer Map. Die Anfragen sind /home 200, /cart 200, /login 401, /home 200, /api/orders 500.

Ausgabe
Anfragen: 5
200: 3
401: 1
500: 1
Erfolgreich: 60%

3. Hausverwaltung. Eine Klasse Building mit einer verschachtelten Klasse Apartment (Nummer, Anzahl der Zimmer, Eigentümer) und den Methoden addApartment(a) und getApartmentInfo(number). Halte die Wohnungen in einer Map von der Nummer zur Wohnung. Füge 101 mit 3 Zimmern von Schmidt und 102 mit 2 Zimmern von Müller hinzu. Suche 101 und 999.

Ausgabe
Wohnung 101: 3 Zimmer, Eigentümer Schmidt
Wohnung 999 gibt es nicht.

4. Rangliste. Sortiere die Schüler aus der Lektion nach Note, von der höchsten abwärts. Bei gleicher Note sortiere sie nach Namen, alphabetisch. Alles steckt in einer Vergleichsfunktion: Sind die Noten verschieden, gib b.grade - a.grade zurück, sonst vergleiche die Namen mit localeCompare.

Ausgabe
Maria 6
Anna 5.5
Jonas 5.5
Peter 4.75

Weitere Aufgaben

  • Computer-Konfigurator. Eine Klasse Computer mit den verschachtelten Klassen CPU (Marke, Takt in GHz), RAM (GB) und Storage (GB) und den Methoden setCPU, setRAM, setStorage. toString beschreibt die Konfiguration: Intel 3.5 GHz, 16 GB RAM, 512 GB.
  • Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über Map einmal mit einer verschachtelten Klasse TripPlanner.Destination, die den Namen und das Array der Aktivitäten hält, und einmal nur mit einer Map von Arrays. Was ist praktischer?
  • Kontaktnetz, noch einmal. Verschiebe Contact aus der Aufgabe in der Lektion über Map in ContactNetwork. Sollte Contact sein Netz kennen?
  • Kopfrechnen, noch einmal. Verschiebe Question aus der Aufgabe in der Lektion über Arrays in MathGame.
  • Produkte auf zwei Arten. Sortiere ein Array von Produkten nach Preis, vom teuersten an, und bei gleichem Preis nach Namen. Sortiere dann dasselbe Array nur nach Namen, in umgekehrter Reihenfolge.
  • Dein eigener Rabatt. Schreib drei Objekte mit einer Methode apply: 10 % ab 100 €, feste 5 € Rabatt und „zweites Produkt zum halben Preis“. Wende sie auf ein und denselben Warenkorb an.

Test

Teste dich

1. Wie machst du in JavaScript eine Klasse, die von außen Order.Line heißt?
2. Sieht Order.Line die Felder einer bestimmten Bestellung?
3. Was ist { apply(price) { return price; } }?
4. Die Vergleichsfunktion von sort gibt für (a, b) eine negative Zahl zurück. Was bedeutet das?
5. students.sort() hat keine Funktion, und Student hat toString(). Was passiert?
6. grid = Array(3).fill([0, 0, 0]) und danach grid[0][0] = 1. Was steht in grid?

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