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)} €`);Gesamt: 37.00 €static Line = class { ... }ist ein statisches Feld vonOrder, dessen Wert eine Klasse ohne Namen ist. Von außen und innerhalb der Methoden heißt sieOrder.Line.new Order.Line(...)entsteht ohne einOrder-Objekt, wie bei einer normalen Klasse.Order.Lineist 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)} €`);KaffeebohnenGesamt: 37.00 €order.linesist das Array,order.lines[0]ist die erste Zeile, undorder.lines[0].productist 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 esMap, 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]);Map(2) { 'Berlin' => [ 'Jonas', 'Anna' ], 'Hamburg' => [ 'Maria' ] }AnnaGenauer, für Neugierige
- Eine Falle bei verschachtelten Arrays:
filllegt 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);[ [ 1, 0, 0 ], [ 1, 0, 0 ], [ 1, 0, 0 ] ][ [ 1, 0, 0 ], [ 0, 0, 0 ], [ 0, 0, 0 ] ]console.logzeigt nur ein paar Ebenen. Weiter unten schreibt es[Array]oder[Object]. Das ganze Objekt zeigtJSON.stringify:
const shop = { name: "Café", menu: { drinks: { hot: ["Kaffee", "Tee"] } } };console.log(shop);console.log(JSON.stringify(shop));{ 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();Kopfhörer: 72.00 €Hülle: 18.00 €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
- Der Warenkorb ist ein Cart-Objekt mit 10% Rabatt.
- 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.
- Das zweite Item zeigt auf denselben Warenkorb.
- finalPrice von „Kopfhörer“ folgt dem Pfeil this.cart und liest #discountPercent: 80 * (100 - 10) / 100 = 72.
- 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, wieaddhier:
const cart = new Cart(10);const cable = new Cart.Item(cart, "Kabel", 10);console.log(cable.finalPrice());9- Das Feld
cartist genau das, was in Java das versteckteCart.thismacht. 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();Jedes Item sieht den Rabatt des Warenkorbs, der es erzeugt hat. Das Buch in regular hat 0 %, das in member 20 %.
Probier es aus
Finde den Fehler
console.log(new Order.Line("Kaffee", 12.5).show()) bricht das Programm ab:ReferenceError: currency is not definedIn welcher Zeile liegt das Problem?
Wähle die Zeile mit dem Fehler.
currency ist ein Feld eines Order-Objekts, und Line hat keine Bestellung. Einen bloßen Namen currency sucht JavaScript als Variable und findet ihn nicht. Der Fehler zeigt sich erst, wenn show aufgerufen wird, nicht früher. Die Lösung: Line bekommt die Bestellung, new Order.Line(order, "Kaffee", 12.5), und liest this.order.currency. Ein anderer Weg ist, currency zu einem statischen Feld zu machen, Order.currency, wenn die Währung für alle Bestellungen dieselbe ist.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);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));70100{ apply(price) { ... } }ist ein Objekt mit einer Methode. Eine Klasse gibt es nicht, undblackFriday.apply(100)funktioniert wie mit einem Objekt einer Klasse.- JavaScript gibt
70aus, nicht70.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 === "");100true- 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(", "));Anna 5.5, Jonas 5.5, Maria 6, Peter 4.75Hier 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(", "));Maria 6, Jonas 5.5, Anna 5.5, Peter 4.75b.grade - a.gradevergleicht andersherum, darum steht die höchste Note vorn.- Jonas und Anna haben dieselbe Note und bleiben in der Reihenfolge, in der sie waren.
sortvertauscht gleiche Elemente nicht. joinrufttoStringfür jeden Schüler auf.- Für Text in umgekehrter Reihenfolge:
sort()und danachreverse().
const cities = ["Hamburg", "Berlin", "München"];cities.sort();cities.reverse();console.log(cities);[ 'München', 'Hamburg', 'Berlin' ]Genauer, für Neugierige
sortsortiert das Array selbst. Brauchst du ein neues, gibt estoSortedmit derselben Funktion.b.grade - a.gradeist sicher, weil Zahlen in JavaScript nicht überlaufen wieintin Java. Für Text gibt es keine Subtraktion: Dort nimmst dua.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);}a.price - b.price ist negativ, wenn a billiger ist. Darum kommt das Billigere zuerst: 25, 60, 220. Damit das Teuerste vorn steht, vertausche a und b.Ü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.
Artikel: 3Gesamt: 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.
Anfragen: 5200: 3401: 1500: 1Erfolgreich: 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.
Wohnung 101: 3 Zimmer, Eigentümer SchmidtWohnung 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.
Maria 6Anna 5.5Jonas 5.5Peter 4.75Weitere Aufgaben
- Computer-Konfigurator. Eine Klasse
Computermit den verschachtelten KlassenCPU(Marke, Takt in GHz),RAM(GB) undStorage(GB) und den MethodensetCPU,setRAM,setStorage.toStringbeschreibt die Konfiguration:Intel 3.5 GHz, 16 GB RAM, 512 GB. - Reiseplaner, noch einmal. Löse die Aufgabe aus der Lektion über
Mapeinmal mit einer verschachtelten KlasseTripPlanner.Destination, die den Namen und das Array der Aktivitäten hält, und einmal nur mit einerMapvon Arrays. Was ist praktischer? - Kontaktnetz, noch einmal. Verschiebe
Contactaus der Aufgabe in der Lektion überMapinContactNetwork. SollteContactsein Netz kennen? - Kopfrechnen, noch einmal. Verschiebe
Questionaus der Aufgabe in der Lektion über Arrays inMathGame. - 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
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Strings · Arrays: push, splice und sort · Stack, Queue und verkettete Liste · Map und Set
- Geht weiter in: Pfeilfunktionen · Array-Methoden · Entwurfsmuster · Tests mit node:test
- Aufgaben zum Thema: Collections