Vollständiger Ausbau des JavaScript & TypeScript Curriculums auf 16 Module (4 Lehrpfade: ES6 Essentials, Moderne Datenmodelle & OOP, DOM & Web APIs, Architecture & TypeScript)
This commit is contained in:
parent
1916a634f4
commit
36cc7a9708
89 changed files with 5515 additions and 132 deletions
|
|
@ -13,5 +13,5 @@
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
|
"zuletzt_aktualisiert": "2026-08-30T10:42:13.626632"
|
||||||
}
|
}
|
||||||
|
|
@ -38,16 +38,20 @@
|
||||||
moduleDesc = "6 Master-Module: Reguläre Ausdrücke (re), Functional Programming (lambda, zip, map), Generatoren & itertools, Dataclasses & Type Hints, Relationale DBs mit SQLite, REST-APIs & JSON-Feeds";
|
moduleDesc = "6 Master-Module: Reguläre Ausdrücke (re), Functional Programming (lambda, zip, map), Generatoren & itertools, Dataclasses & Type Hints, Relationale DBs mit SQLite, REST-APIs & JSON-Feeds";
|
||||||
trackKey = "lehrpfad_4";
|
trackKey = "lehrpfad_4";
|
||||||
} else if (lehrpfadNummer === "js_grundlagen") {
|
} else if (lehrpfadNummer === "js_grundlagen") {
|
||||||
titel = "JavaScript ES6+ Grundlagen & DOM-Manipulation";
|
titel = "JavaScript Grundlagen, Kontrollfluss & ES6";
|
||||||
moduleDesc = "4 Module: Variablen (let/const), Arrow Functions, Arrays (map/filter/reduce), Objekte, Destructuring und DOM-Event-Driven Architecture";
|
moduleDesc = "5 Module: Variablen (let/const), Verzweigungen & Schleifen, Funktionen & Closures, Arrays/Sets/Maps und Objekte & Destructuring";
|
||||||
trackKey = "js_grundlagen";
|
trackKey = "js_grundlagen";
|
||||||
|
} else if (lehrpfadNummer === "js_oop") {
|
||||||
|
titel = "Moderne ES6+ Datenmodelle, OOP & Robustes Debugging";
|
||||||
|
moduleDesc = "3 Module: ES6-Klassen & Vererbung, Reguläre Ausdrücke (RegExp) & strukturierte Fehlerbehandlung mit Error-Klassen";
|
||||||
|
trackKey = "js_oop";
|
||||||
} else if (lehrpfadNummer === "js_async") {
|
} else if (lehrpfadNummer === "js_async") {
|
||||||
titel = "Asynchrones JavaScript & REST-APIs";
|
titel = "Web-APIs, DOM-Manipulation & Asynchrones JavaScript";
|
||||||
moduleDesc = "2 Module: Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit Fetch & JSON";
|
moduleDesc = "4 Module: DOM Event-Delegation, Web Storage API (LocalStorage), Promises/Chaining und Async/Await mit Fetch";
|
||||||
trackKey = "js_async";
|
trackKey = "js_async";
|
||||||
} else if (lehrpfadNummer === "js_master") {
|
} else if (lehrpfadNummer === "js_master") {
|
||||||
titel = "Master of Modern JavaScript & TypeScript Development";
|
titel = "Master of Modern JavaScript & TypeScript Engineering";
|
||||||
moduleDesc = "Vollständiger Abschluss des JS/TS Curriculums (8 Module, TypeScript Type Safety, Interfaces, Generics & Task-App Masterprojekt)";
|
moduleDesc = "Vollständiger Abschluss des JS/TS Curriculums (16 Module, ES-Module, TypeScript Interfaces/Generics & MVC Task-App)";
|
||||||
trackKey = "js_master";
|
trackKey = "js_master";
|
||||||
} else if (lehrpfadNummer === "sql_grundlagen") {
|
} else if (lehrpfadNummer === "sql_grundlagen") {
|
||||||
titel = "SQL & Relationale Datenbanken";
|
titel = "SQL & Relationale Datenbanken";
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ window.GAMIFICATION_DATA = {
|
||||||
"streak_tage": 1,
|
"streak_tage": 1,
|
||||||
"letzter_aktiver_tag": "2026-08-29",
|
"letzter_aktiver_tag": "2026-08-29",
|
||||||
"historie": [],
|
"historie": [],
|
||||||
"zuletzt_aktualisiert": "2026-08-30T00:42:10.922005"
|
"zuletzt_aktualisiert": "2026-08-30T10:42:13.626632"
|
||||||
};
|
};
|
||||||
window.GAMIFICATION_BADGES_KATALOG = {
|
window.GAMIFICATION_BADGES_KATALOG = {
|
||||||
"rechenkonegin": {
|
"rechenkonegin": {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
const BLOCKED_JS_PATTERNS = [
|
const BLOCKED_JS_PATTERNS = [
|
||||||
/(?:parent|top|opener)\.document/i,
|
/(?:parent|top|opener)\.document/i,
|
||||||
/(?:window|document)\.location/i,
|
/(?:window|document)\.location/i,
|
||||||
/localStorage|sessionStorage|indexedDB/i,
|
/(?:window|parent|top)\.(?:localStorage|sessionStorage|indexedDB)/i,
|
||||||
/document\.cookie/i
|
/document\.cookie/i
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
||||||
30
courses/javascript/01_erste_schritte_variablen/README.md
Normal file
30
courses/javascript/01_erste_schritte_variablen/README.md
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
# JS 01: Variablen, Typen & Template-Strings 🌱
|
||||||
|
|
||||||
|
Willkommen zu **Modul 01** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
In diesem Modul lernst du die Grundlagen von modernen JavaScript-Variablen (`let`, `const`), Basis-Datentypen und Template-Strings (`${...}`) kennen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### `let` vs. `const`
|
||||||
|
- `const`: Für Werte, deren Bindung sich nicht ändert. Immer die Standard-Wahl!
|
||||||
|
- `let`: Für Variablen, deren Wert später neu zugewiesen wird (z.B. Zähler in Schleifen).
|
||||||
|
- `var`: Veraltet (ES5), bitte in modernem Code nicht mehr verwenden.
|
||||||
|
|
||||||
|
### Template-Strings
|
||||||
|
Mit Backticks (`` ` ``) kannst du Ausdrücke und Variablen direkt in Strings einbetten:
|
||||||
|
```javascript
|
||||||
|
const name = "Max";
|
||||||
|
const alter = 25;
|
||||||
|
const nachricht = `Hallo ${name}, du bist ${alter} Jahre alt.`;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben in `aufgabe.js`
|
||||||
|
|
||||||
|
1. Deklariere die Konstante `KURS_NAME = "JavaScript Masterclass"`.
|
||||||
|
2. Erstelle Variablen für Name und Alter.
|
||||||
|
3. Formatiere einen Begrüßungssatz mit einem Template-String.
|
||||||
|
|
@ -47,7 +47,7 @@ console.log(text);</code></pre>
|
||||||
|
|
||||||
<div style="margin-top: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=01_erste_schritte_variablen" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=01_erste_schritte_variablen" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">Weiter zu JS 02 →</a>
|
<a href="../02_kontrollfluss_und_schleifen/index.html" class="btn btn-secondary">Weiter zu JS 02 →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
/**
|
|
||||||
* 🌐 JS 02: ARROW FUNCTIONS, ARRAYS & MAP/FILTER 🌐
|
|
||||||
* =================================================
|
|
||||||
*/
|
|
||||||
|
|
||||||
// 🎯 TEILZIEL 1 (TODO 1): Schreibe verdoppeln(zahlen), die ein neues Array mit allen Zahlen verdoppelt zurückgibt (z.B. mit zahlen.map()).
|
|
||||||
function verdoppeln(zahlen) {
|
|
||||||
// TODO: Array verdoppeln
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎯 TEILZIEL 2 (TODO 2): Schreibe filterGeradeZahlen(zahlen), die nur gerade Zahlen filtert (mit zahlen.filter()).
|
|
||||||
function filterGeradeZahlen(zahlen) {
|
|
||||||
// TODO: Nur gerade Zahlen
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 🎯 TEILZIEL 3 (TODO 3): Schreibe arraySumme(zahlen), die die Gesamtsumme berechnet (z.B. mit zahlen.reduce()).
|
|
||||||
function arraySumme(zahlen) {
|
|
||||||
// TODO: Summe berechnen
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="de">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>JS 02: Arrow Functions & Array-Methoden</title>
|
|
||||||
<link rel="stylesheet" href="../../../assets/style.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="app-layout">
|
|
||||||
<div class="app-main">
|
|
||||||
<header>
|
|
||||||
<div class="header-container">
|
|
||||||
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
|
||||||
<h1>JS 02: Arrow Functions, Arrays & Map/Filter</h1>
|
|
||||||
<p>Lerne funktionale Array-Transformationen mit <code>.map()</code>, <code>.filter()</code> und <code>.reduce()</code>.</p>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="container">
|
|
||||||
<section class="card">
|
|
||||||
<h2>💡 1. Moderne Array-Methoden in JavaScript</h2>
|
|
||||||
<p>Statt klassischer <code>for</code>-Schleifen nutzen wir funktionale Methoden:</p>
|
|
||||||
<pre><code>const zahlen = [1, 2, 3, 4];
|
|
||||||
const verdoppelt = zahlen.map(x => x * 2);
|
|
||||||
const gerade = zahlen.filter(x => x % 2 === 0);
|
|
||||||
const summe = zahlen.reduce((a, b) => a + b, 0);</code></pre>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card">
|
|
||||||
<h2>🎯 Aufgaben & Checkliste</h2>
|
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t1">
|
|
||||||
<label class="task-label" for="t1"><strong>TODO 1:</strong> Funktion <code>verdoppeln(zahlen)</code> mit <code>.map()</code> schreiben.</label>
|
|
||||||
</div>
|
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t2">
|
|
||||||
<label class="task-label" for="t2"><strong>TODO 2:</strong> Funktion <code>filterGeradeZahlen(zahlen)</code> mit <code>.filter()</code> schreiben.</label>
|
|
||||||
</div>
|
|
||||||
<div class="task-item">
|
|
||||||
<input type="checkbox" class="task-checkbox" id="t3">
|
|
||||||
<label class="task-label" for="t3"><strong>TODO 3:</strong> Funktion <code>arraySumme(zahlen)</code> mit <code>.reduce()</code> schreiben.</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div style="margin-top: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
|
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=02_funktionen_und_arrays" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
|
||||||
<a href="../01_erste_schritte_variablen/index.html" class="btn btn-secondary">← Zurück zu JS 01</a>
|
|
||||||
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">Weiter zu JS 03 →</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
function verdoppeln(zahlen) {
|
|
||||||
return zahlen.map(x => x * 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
function filterGeradeZahlen(zahlen) {
|
|
||||||
return zahlen.filter(x => x % 2 === 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function arraySumme(zahlen) {
|
|
||||||
return zahlen.reduce((acc, curr) => acc + curr, 0);
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
assert.deepStrictEqual(verdoppeln([1, 2, 3]), [2, 4, 6], "verdoppeln([1, 2, 3]) sollte [2, 4, 6] sein");
|
|
||||||
assert.deepStrictEqual(filterGeradeZahlen([1, 2, 3, 4, 5, 6]), [2, 4, 6], "filterGeradeZahlen() filtert unvollständig");
|
|
||||||
assert.strictEqual(arraySumme([10, 20, 30]), 60, "arraySumme([10, 20, 30]) sollte 60 sein");
|
|
||||||
assert.strictEqual(arraySumme([]), 0, "arraySumme([]) sollte 0 sein");
|
|
||||||
console.log("✅ Alle 4 JavaScript Tests in JS 02 erfolgreich bestanden!");
|
|
||||||
200
courses/javascript/02_kontrollfluss_und_schleifen/README.md
Normal file
200
courses/javascript/02_kontrollfluss_und_schleifen/README.md
Normal file
|
|
@ -0,0 +1,200 @@
|
||||||
|
# JS 02: Kontrollfluss, Verzweigungen & Schleifen 🚦
|
||||||
|
|
||||||
|
Willkommen zu **Modul 02** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
In realen Programmen läuft Code selten nur starr von oben nach unten ab. Ein Programm muss Entscheidungen treffen ("Ist der Benutzer eingeloggt?", "Hat der Kunde einen Rabatt?") und Aktionen wiederholen ("Durchsuche 1000 Produkte", "Summiere Zahlen").
|
||||||
|
|
||||||
|
In diesem Modul lernst du alle Werkzeuge kennen, mit denen du den Kontrollfluss deines JavaScript-Codes präzise und elegant steuerst.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. Verzweigungen: `if`, `else if`, `else`
|
||||||
|
Mit Verzweigungen führst du Codeblöcke nur dann aus, wenn bestimmte Bedingungen erfüllt (`true`) sind:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const punkte = 85;
|
||||||
|
|
||||||
|
if (punkte >= 90) {
|
||||||
|
console.log("Note: Sehr gut");
|
||||||
|
} else if (punkte >= 75) {
|
||||||
|
console.log("Note: Gut");
|
||||||
|
} else if (punkte >= 50) {
|
||||||
|
console.log("Note: Bestanden");
|
||||||
|
} else {
|
||||||
|
console.log("Note: Nicht bestanden");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ⚠️ Strikter Vergleich: `===` vs. `==`
|
||||||
|
Verwende in modernem JavaScript **immer den strikten Gleichheitsoperator `===`** (und Ungleichheit `!==`):
|
||||||
|
- `===` prüft Wert **und Datentyp** (ohne implizite Typkonvertierung).
|
||||||
|
- `==` führt vorher eine automatische Typumwandlung (*Type Coercion*) durch, was zu tückischen Bugs führt (`0 == ""` ist `true`, aber `0 === ""` ist `false`).
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log(5 === "5"); // false (Zahl vs. String)
|
||||||
|
console.log(5 == "5"); // true (Achtung: Gefährlich!)
|
||||||
|
console.log(null === undefined); // false
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Die `switch-case`-Anweisung
|
||||||
|
Wenn eine einzelne Variable gegen viele diskrete Werte geprüft werden soll, ist `switch` oft übersichtlicher als endlose Ketten von `else if`:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const status = "geliefert";
|
||||||
|
|
||||||
|
switch (status) {
|
||||||
|
case "bestellt":
|
||||||
|
console.log("Paket wird verpackt.");
|
||||||
|
break; // Verhindert 'Fall-Through' zum nächsten Case!
|
||||||
|
case "unterwegs":
|
||||||
|
console.log("Paket ist im Zustellfahrzeug.");
|
||||||
|
break;
|
||||||
|
case "geliefert":
|
||||||
|
console.log("Paket erfolgreich zugestellt!");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unbekannter Status.");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Wichtig:** Vergiss niemals das `break;` am Ende jedes Case-Blocks, außer du möchtest bewusst mehrere Cases zusammenfassen (*Fall-Through*).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Der Ternäre Operator (`? :`)
|
||||||
|
Für einfache Entweder-Oder-Zuweisungen bietet der ternäre Operator eine kompakte Einzeiler-Syntax:
|
||||||
|
|
||||||
|
$$\text{Bedingung} \ ? \ \text{Wert wenn wahr} \ : \ \text{Wert wenn falsch}$$
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const alter = 20;
|
||||||
|
const status = alter >= 18 ? "Erwachsen" : "Minderjährig";
|
||||||
|
console.log(status); // "Erwachsen"
|
||||||
|
|
||||||
|
// Auch kombinierbar für Rabatte:
|
||||||
|
const istPremium = true;
|
||||||
|
const rabatt = istPremium ? 0.2 : 0.0;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D. Schleifen in JavaScript
|
||||||
|
|
||||||
|
#### 1. Die klassische Zählschleife: `for (let i = 0; i < n; i++)`
|
||||||
|
Ideal, wenn du die genaue Anzahl der Durchläufe kennst oder den Index benötigst:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
console.log(`Durchlauf Nummer: ${i}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Die moderne Iterationsschleife: `for (const element of array)`
|
||||||
|
Perfekt zum Durchlaufen von Arrays und iterierbaren Datenstrukturen:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const fruechte = ["Apfel", "Banane", "Mango"];
|
||||||
|
for (const frucht of fruechte) {
|
||||||
|
console.log(`Lecker: ${frucht}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Die Objekt-Schlüsselschleife: `for (const key in object)`
|
||||||
|
Durchläuft alle Schlüssel (*Keys*) eines Objekts:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const auto = { marke: "BMW", baujahr: 2023, ps: 190 };
|
||||||
|
for (const eigenschaft in auto) {
|
||||||
|
console.log(`${eigenschaft}: ${auto[eigenschaft]}`);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Bedingungsgesteuerte Schleifen: `while` & `do...while`
|
||||||
|
- `while (bedingung)`: Prüft die Bedingung **vor** dem ersten Durchlauf.
|
||||||
|
- `do { ... } while (bedingung)`: Führt den Codeblock **mindestens einmal** aus, bevor geprüft wird.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
let energie = 100;
|
||||||
|
while (energie > 0) {
|
||||||
|
console.log(`Akku bei ${energie}%`);
|
||||||
|
energie -= 25;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### E. Schleifensteuerung: `break` und `continue`
|
||||||
|
|
||||||
|
- **`break`**: Bricht die gesamte Schleife sofort ab und springt hinter die Schleife.
|
||||||
|
- **`continue`**: Bricht nur den *aktuellen Durchlauf* ab und springt direkt zur nächsten Iteration.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Beispiel: Nur ungerade Zahlen überspringen (continue) und bei 8 stoppen (break)
|
||||||
|
for (let i = 1; i <= 10; i++) {
|
||||||
|
if (i % 2 !== 0) {
|
||||||
|
continue; // Ungerade Zahl -> Weiter zur nächsten Zahl
|
||||||
|
}
|
||||||
|
if (i > 8) {
|
||||||
|
break; // Beende Schleife vorzeitig
|
||||||
|
}
|
||||||
|
console.log(i); // Gibt 2, 4, 6, 8 aus
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚦 Die didaktische Analogie: "Die Verkehrsampeln & Kreisverkehre im Code"
|
||||||
|
|
||||||
|
Stelle dir den Programmablauf wie eine Autofahrt durch eine moderne Stadt vor:
|
||||||
|
|
||||||
|
- 🟢 **Die Ampelkreuzung (`if` / `else` / `switch`):**
|
||||||
|
Dein Auto rollt auf eine Kreuzung zu. Steht die Ampel auf Grün (`true`), fährst du geradeaus weiter. Zeigt sie Rot (`false`), biegst du in die Ausweichroute (`else`) ab. Eine `switch`-Kreuzung ist ein mehrspuriger Verteiler: Jede Spur führt zu einem anderen Ziel (`case`), und wenn keine passt, nimmst du die Notspur (`default`).
|
||||||
|
|
||||||
|
- 🔄 **Der Kreisverkehr (`while` / `for`):**
|
||||||
|
Schleifen sind wie ein Kreisverkehr. Du drehst solange deine Runden, bis eine bestimmte Bedingung nicht mehr erfüllt ist oder du deine gewünschte Ausfahrt entdeckst.
|
||||||
|
- Mit **`continue`** bleibst du auf der Innenbahn und fährst ohne Anhalten direkt in die nächste Runde.
|
||||||
|
- Mit **`break`** nimmst du sofort die Ausfahrt und verlässt den Kreisverkehr endgültig.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
Öffne die Datei `aufgabe.js` und implementiere folgende 5 Funktionen:
|
||||||
|
|
||||||
|
1. **TODO 1: `kategorisiereAlter(alter)`**
|
||||||
|
Kategorisiert ein Alter in String-Gruppen:
|
||||||
|
- `< 13` $\rightarrow$ `"Kind"`
|
||||||
|
- `13` bis `17` $\rightarrow$ `"Jugendlicher"`
|
||||||
|
- `18` bis `64` $\rightarrow$ `"Erwachsener"`
|
||||||
|
- $\ge 65$ $\rightarrow$ `"Senior"`
|
||||||
|
|
||||||
|
2. **TODO 2: `wochentagName(tagNummer)`**
|
||||||
|
Wandelt eine Zahl `1` bis `7` in den Wochentag `"Montag"` bis `"Sonntag"` um. Alle anderen Eingaben liefern `"Ungültig"`. Verwende `switch(tagNummer)`.
|
||||||
|
|
||||||
|
3. **TODO 3: `berechneRabattpreis(preis, istPremium, hatGutschein)`**
|
||||||
|
Berechnet den rabattierten Endpreis:
|
||||||
|
- Premium + Gutschein $\rightarrow$ 30% Rabatt (`preis * 0.7`)
|
||||||
|
- Nur Premium $\rightarrow$ 20% Rabatt (`preis * 0.8`)
|
||||||
|
- Nur Gutschein $\rightarrow$ 10% Rabatt (`preis * 0.9`)
|
||||||
|
- Weder noch $\rightarrow$ kein Rabatt (`preis`)
|
||||||
|
|
||||||
|
4. **TODO 4: `summeGeraderZahlen(start, ende)`**
|
||||||
|
Summiert alle geraden Zahlen im Intervall $[start, ende]$. Verwende eine `for`-Schleife und `continue` für ungerade Zahlen.
|
||||||
|
|
||||||
|
5. **TODO 5: `findeErstesVielfaches(zahlen, teiler)`**
|
||||||
|
Durchsucht das Array `zahlen` mit `for...of` nach der ersten Zahl, für die `zahl % teiler === 0` gilt. Beende die Schleife mit `break` und gib die gefundene Zahl zurück (oder `null`, wenn kein Vielfaches existiert).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE im Browser oder starte den Testrunner:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Im Browser:
|
||||||
|
workspace.html?course=javascript&track=track_1_grundlagen&chapter=02_kontrollfluss_und_schleifen
|
||||||
|
```
|
||||||
59
courses/javascript/02_kontrollfluss_und_schleifen/aufgabe.js
Normal file
59
courses/javascript/02_kontrollfluss_und_schleifen/aufgabe.js
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 02: KONTROLLFLUSS, VERZWEIGUNGEN & SCHLEIFEN 🌐
|
||||||
|
* ====================================================
|
||||||
|
* Entscheidungen treffen und Wiederholungen steuern in JavaScript.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): kategorisiereAlter(alter)
|
||||||
|
// Bestimme die Altersgruppe anhand folgender Kriterien:
|
||||||
|
// - kleiner als 13: "Kind"
|
||||||
|
// - 13 bis 17 (inklusive): "Jugendlicher"
|
||||||
|
// - 18 bis 64 (inklusive): "Erwachsener"
|
||||||
|
// - ab 65 (inklusive): "Senior"
|
||||||
|
// Verwende if, else if und else mit striktem Vergleich.
|
||||||
|
function kategorisiereAlter(alter) {
|
||||||
|
// TODO: Implementiere die Alterskategorisierung
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): wochentagName(tagNummer)
|
||||||
|
// Wandle eine Zahl von 1 bis 7 in den passenden Wochentag um:
|
||||||
|
// 1 -> "Montag", 2 -> "Dienstag", 3 -> "Mittwoch", 4 -> "Donnerstag",
|
||||||
|
// 5 -> "Freitag", 6 -> "Samstag", 7 -> "Sonntag"
|
||||||
|
// Für alle anderen Zahlen/Eingaben soll "Ungültig" zurückgegeben werden.
|
||||||
|
// Verwende dafür eine switch-case Anweisung mit break und default.
|
||||||
|
function wochentagName(tagNummer) {
|
||||||
|
// TODO: Nutze switch(tagNummer) { case ...: return ...; default: ... }
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): berechneRabattpreis(preis, istPremium, hatGutschein)
|
||||||
|
// Berechne den rabattierten Endpreis:
|
||||||
|
// - Wenn istPremium UND hatGutschein: 30% Rabatt (zahlt 70%, also preis * 0.7)
|
||||||
|
// - Wenn nur istPremium: 20% Rabatt (zahlt 80%, also preis * 0.8)
|
||||||
|
// - Wenn nur hatGutschein: 10% Rabatt (zahlt 90%, also preis * 0.9)
|
||||||
|
// - Sonst: kein Rabatt (zahlt 100%, also preis)
|
||||||
|
// Tipp: Nutze if/else oder den ternären Operator.
|
||||||
|
function berechneRabattpreis(preis, istPremium, hatGutschein) {
|
||||||
|
// TODO: Berechne den reduzierten Preis
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): summeGeraderZahlen(start, ende)
|
||||||
|
// Berechne die Summe aller GERADEN Zahlen im Bereich von start bis einschließlich ende.
|
||||||
|
// Nutze eine for-Schleife (for (let i = start; i <= ende; i++)) und
|
||||||
|
// überspringe ungerade Zahlen mit 'continue'.
|
||||||
|
function summeGeraderZahlen(start, ende) {
|
||||||
|
// TODO: for-Schleife mit continue für ungerade Zahlen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 5 (TODO 5): findeErstesVielfaches(zahlen, teiler)
|
||||||
|
// Durchsuche das Array 'zahlen' nach der ersten Zahl, die ohne Rest durch 'teiler'
|
||||||
|
// teilbar ist (zahl % teiler === 0).
|
||||||
|
// Verwende eine for...of Schleife und beende die Schleife vorzeitig mit 'break' (oder direktem return).
|
||||||
|
// Wird kein Vielfaches gefunden oder ist das Array leer, gib null zurück.
|
||||||
|
function findeErstesVielfaches(zahlen, teiler) {
|
||||||
|
// TODO: for (const zahl of zahlen) mit Prüfung und break
|
||||||
|
return null;
|
||||||
|
}
|
||||||
186
courses/javascript/02_kontrollfluss_und_schleifen/index.html
Normal file
186
courses/javascript/02_kontrollfluss_und_schleifen/index.html
Normal file
|
|
@ -0,0 +1,186 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 02: Kontrollfluss & Schleifen</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
||||||
|
<h1>JS 02: Kontrollfluss, Verzweigungen & Schleifen</h1>
|
||||||
|
<p>Lerne Entscheidungslogik mit <code>if/else</code>, <code>switch</code> und den mächtigen Schleifenkonstrukten in modernem JavaScript.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: IF / ELSE & STRIKTER VERGLEICH -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Verzweigungen & Strikter Vergleich (<code>===</code>)</h2>
|
||||||
|
<p>Bedingte Anweisungen steuern, welche Codeblöcke bei bestimmten Bedingungen ausgeführt werden. In modernem JavaScript verwenden wir ausnahmslos den <strong>strikten Vergleich <code>===</code></strong> (bzw. <code>!==</code>), um ungewollte Typumwandlungen zu verhindern:</p>
|
||||||
|
|
||||||
|
<pre><code>const alter = 17;
|
||||||
|
|
||||||
|
if (alter >= 18) {
|
||||||
|
console.log("Zugang gewährt: Volljährig");
|
||||||
|
} else if (alter >= 13) {
|
||||||
|
console.log("Jugendschutz: Eingeschränkter Zugang");
|
||||||
|
} else {
|
||||||
|
console.log("Kindersicherung aktiv");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ⚠️ Strikter Vergleich:
|
||||||
|
console.log(42 === "42"); // false (Number !== String)
|
||||||
|
console.log(42 == "42"); // true (Achtung: Gefährliche implizite Konvertierung!)</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: SWITCH-CASE & TERNARY -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔀 2. Switch-Case & Ternärer Operator</h2>
|
||||||
|
<p>Wenn du eine Variable gegen viele konkrete Werte prüfen möchtest, sorgt <code>switch</code> für sauberen Code. Für schnelle Zuweisungen ist der ternäre Operator (<code>? :</code>) die beste Wahl:</p>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 Switch-Case:
|
||||||
|
const rolle = "admin";
|
||||||
|
switch (rolle) {
|
||||||
|
case "admin":
|
||||||
|
console.log("Volle Administrationsrechte");
|
||||||
|
break; // Verhindert 'Fall-Through'
|
||||||
|
case "editor":
|
||||||
|
console.log("Inhalte bearbeiten");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Gastzugang (Nur Lesen)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 Ternärer Operator (Bedingung ? Wenn-Wahr : Wenn-Falsch):
|
||||||
|
const hatRabatt = true;
|
||||||
|
const endpreis = hatRabatt ? 80 : 100;</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: SCHLEIFEN & CONTROL -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔄 3. Schleifen: <code>for</code>, <code>for...of</code>, <code>while</code></h2>
|
||||||
|
<p>Wiederhole Operationen präzise. Mit <code>break</code> brichst du eine Schleife vorzeitig ab, mit <code>continue</code> springst du sofort zum nächsten Durchlauf:</p>
|
||||||
|
|
||||||
|
<pre><code>// 1. Zählschleife:
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
if (i === 3) continue; // Überspringt die 3
|
||||||
|
console.log(i); // 1, 2, 4, 5
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Iteration über Arrays (for...of):
|
||||||
|
const zutaten = ["Mehl", "Zucker", "Eier"];
|
||||||
|
for (const zutat of zutaten) {
|
||||||
|
console.log("Zutat:", zutat);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Bedingungsschleife:
|
||||||
|
let countdown = 3;
|
||||||
|
while (countdown > 0) {
|
||||||
|
console.log(countdown--);
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🚦 Didaktische Analogie: Die Verkehrsampeln & Kreisverkehre im Code</div>
|
||||||
|
<p><strong>Die Ampelkreuzung (<code>if</code>/<code>else</code>/<code>switch</code>):</strong> Je nachdem, welches Signal aufleuchtet, nimmt dein Datenstrom eine andere Abbiegung. <code>switch</code> ist der mehrspurige Kreisverkehr mit festen Ausfahrten.</p>
|
||||||
|
<p><strong>Der Kreisverkehr (<code>while</code>/<code>for</code>):</strong> Dein Code kreist so lange, wie die Bedingung wahr ist. Mit <code>continue</code> fährst du ohne Halt in die nächste Runde, mit <code>break</code> nimmst du sofort die nächste Ausfahrt ins Freie!</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: Kontrollfluss & Schleifen</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Konstrukt</th>
|
||||||
|
<th style="padding: 10px;">Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Zweck</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>if / else if / else</code></td>
|
||||||
|
<td style="padding: 10px;"><code>if (x > 0) { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Verzweigung nach Wahrheitswerten</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>switch(wert) { case ...: }</code></td>
|
||||||
|
<td style="padding: 10px;"><code>case 1: return "Mo"; break;</code></td>
|
||||||
|
<td style="padding: 10px;">Gezielte Prüfung auf diskrete Werte</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>bed ? a : b</code></td>
|
||||||
|
<td style="padding: 10px;"><code>const s = a >= 18 ? 'Erw' : 'Kind';</code></td>
|
||||||
|
<td style="padding: 10px;">Kompakter ternärer Ausdruck</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>for (let i = 0; i < n; i++)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>for (let i = 0; i < 10; i++)</code></td>
|
||||||
|
<td style="padding: 10px;">Zählschleife mit Index-Schritten</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>for (const x of array)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>for (const item of items)</code></td>
|
||||||
|
<td style="padding: 10px;">Direkte Element-Iteration</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>while (bed) / do..while</code></td>
|
||||||
|
<td style="padding: 10px;"><code>while (n > 0) n--;</code></td>
|
||||||
|
<td style="padding: 10px;">Bedingungsgesteuerte Schleifen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>break / continue</code></td>
|
||||||
|
<td style="padding: 10px;"><code>if (i % 2 !== 0) continue;</code></td>
|
||||||
|
<td style="padding: 10px;">Schleifenabbruch / Nächster Durchlauf</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>kategorisiereAlter(alter)</code> mit <code>if / else if</code> (Kind, Jugendlicher, Erwachsener, Senior) implementieren.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>wochentagName(tagNummer)</code> mit <code>switch-case</code> (1=Montag..7=Sonntag, sonst "Ungültig") schreiben.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>berechneRabattpreis(preis, istPremium, hatGutschein)</code> mit Rabattlogik (30%, 20%, 10%, 0%) umsetzen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>summeGeraderZahlen(start, ende)</code> mit <code>for</code>-Schleife und <code>continue</code> berechnen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t5">
|
||||||
|
<label class="task-label" for="t5"><strong>TODO 5:</strong> <code>findeErstesVielfaches(zahlen, teiler)</code> mit <code>for...of</code> und <code>break</code> lösen.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 4 & 5</div>
|
||||||
|
<p>Für ungerade Zahlen kannst du <code>if (i % 2 !== 0) continue;</code> schreiben. Bei <code>findeErstesVielfaches</code> beende die Schleife mit <code>break</code>, sobald <code>zahl % teiler === 0</code> zutrifft!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=02_kontrollfluss_und_schleifen" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../01_erste_schritte_variablen/index.html" class="btn btn-secondary">← Zurück zu JS 01</a>
|
||||||
|
<a href="../03_funktionen_und_scope/index.html" class="btn btn-secondary">Weiter zu JS 03 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
function kategorisiereAlter(alter) {
|
||||||
|
if (alter < 13) {
|
||||||
|
return "Kind";
|
||||||
|
} else if (alter <= 17) {
|
||||||
|
return "Jugendlicher";
|
||||||
|
} else if (alter <= 64) {
|
||||||
|
return "Erwachsener";
|
||||||
|
} else {
|
||||||
|
return "Senior";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function wochentagName(tagNummer) {
|
||||||
|
switch (tagNummer) {
|
||||||
|
case 1:
|
||||||
|
return "Montag";
|
||||||
|
case 2:
|
||||||
|
return "Dienstag";
|
||||||
|
case 3:
|
||||||
|
return "Mittwoch";
|
||||||
|
case 4:
|
||||||
|
return "Donnerstag";
|
||||||
|
case 5:
|
||||||
|
return "Freitag";
|
||||||
|
case 6:
|
||||||
|
return "Samstag";
|
||||||
|
case 7:
|
||||||
|
return "Sonntag";
|
||||||
|
default:
|
||||||
|
return "Ungültig";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function berechneRabattpreis(preis, istPremium, hatGutschein) {
|
||||||
|
let rabattSatz = 0;
|
||||||
|
if (istPremium && hatGutschein) {
|
||||||
|
rabattSatz = 0.3;
|
||||||
|
} else if (istPremium) {
|
||||||
|
rabattSatz = 0.2;
|
||||||
|
} else if (hatGutschein) {
|
||||||
|
rabattSatz = 0.1;
|
||||||
|
}
|
||||||
|
return preis * (1 - rabattSatz);
|
||||||
|
}
|
||||||
|
|
||||||
|
function summeGeraderZahlen(start, ende) {
|
||||||
|
let summe = 0;
|
||||||
|
for (let i = start; i <= ende; i++) {
|
||||||
|
if (i % 2 !== 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
summe += i;
|
||||||
|
}
|
||||||
|
return summe;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findeErstesVielfaches(zahlen, teiler) {
|
||||||
|
let ergebnis = null;
|
||||||
|
for (const zahl of zahlen) {
|
||||||
|
if (zahl % teiler === 0) {
|
||||||
|
ergebnis = zahl;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
// 🧪 Testsuite: JS 02 - Kontrollfluss, Verzweigungen & Schleifen
|
||||||
|
|
||||||
|
// Test 1: kategorisiereAlter
|
||||||
|
assert.strictEqual(kategorisiereAlter(5), "Kind", "Alter 5 sollte 'Kind' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(12), "Kind", "Alter 12 sollte 'Kind' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(13), "Jugendlicher", "Alter 13 sollte 'Jugendlicher' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(17), "Jugendlicher", "Alter 17 sollte 'Jugendlicher' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(18), "Erwachsener", "Alter 18 sollte 'Erwachsener' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(64), "Erwachsener", "Alter 64 sollte 'Erwachsener' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(65), "Senior", "Alter 65 sollte 'Senior' sein");
|
||||||
|
assert.strictEqual(kategorisiereAlter(80), "Senior", "Alter 80 sollte 'Senior' sein");
|
||||||
|
|
||||||
|
// Test 2: wochentagName
|
||||||
|
assert.strictEqual(wochentagName(1), "Montag", "Tag 1 sollte 'Montag' sein");
|
||||||
|
assert.strictEqual(wochentagName(3), "Mittwoch", "Tag 3 sollte 'Mittwoch' sein");
|
||||||
|
assert.strictEqual(wochentagName(7), "Sonntag", "Tag 7 sollte 'Sonntag' sein");
|
||||||
|
assert.strictEqual(wochentagName(0), "Ungültig", "Tag 0 sollte 'Ungültig' sein");
|
||||||
|
assert.strictEqual(wochentagName(8), "Ungültig", "Tag 8 sollte 'Ungültig' sein");
|
||||||
|
assert.strictEqual(wochentagName(-2), "Ungültig", "Tag -2 sollte 'Ungültig' sein");
|
||||||
|
|
||||||
|
// Test 3: berechneRabattpreis
|
||||||
|
assert.strictEqual(berechneRabattpreis(100, true, true), 70, "100€ mit Premium & Gutschein (30%) sollte 70€ ergeben");
|
||||||
|
assert.strictEqual(berechneRabattpreis(100, true, false), 80, "100€ nur Premium (20%) sollte 80€ ergeben");
|
||||||
|
assert.strictEqual(berechneRabattpreis(100, false, true), 90, "100€ nur Gutschein (10%) sollte 90€ ergeben");
|
||||||
|
assert.strictEqual(berechneRabattpreis(100, false, false), 100, "100€ ohne Rabatte sollte 100€ ergeben");
|
||||||
|
assert.strictEqual(berechneRabattpreis(50, true, true), 35, "50€ mit 30% Rabatt sollte 35€ ergeben");
|
||||||
|
|
||||||
|
// Test 4: summeGeraderZahlen
|
||||||
|
assert.strictEqual(summeGeraderZahlen(1, 10), 30, "Summe gerader Zahlen 1 bis 10 (2+4+6+8+10) sollte 30 sein");
|
||||||
|
assert.strictEqual(summeGeraderZahlen(1, 1), 0, "Summe gerader Zahlen von 1 bis 1 sollte 0 sein");
|
||||||
|
assert.strictEqual(summeGeraderZahlen(2, 2), 2, "Summe gerader Zahlen von 2 bis 2 sollte 2 sein");
|
||||||
|
assert.strictEqual(summeGeraderZahlen(4, 8), 18, "Summe gerader Zahlen 4 bis 8 (4+6+8) sollte 18 sein");
|
||||||
|
assert.strictEqual(summeGeraderZahlen(5, 5), 0, "Summe gerader Zahlen 5 bis 5 sollte 0 sein");
|
||||||
|
|
||||||
|
// Test 5: findeErstesVielfaches
|
||||||
|
assert.strictEqual(findeErstesVielfaches([7, 14, 21], 7), 7, "Erstes Vielfaches von 7 in [7, 14, 21] sollte 7 sein");
|
||||||
|
assert.strictEqual(findeErstesVielfaches([5, 11, 18, 25], 6), 18, "Erstes Vielfaches von 6 in [5, 11, 18, 25] sollte 18 sein");
|
||||||
|
assert.strictEqual(findeErstesVielfaches([1, 3, 5, 7], 2), null, "Kein Vielfaches von 2 vorhanden, sollte null sein");
|
||||||
|
assert.strictEqual(findeErstesVielfaches([], 5), null, "Leeres Array sollte null liefern");
|
||||||
|
assert.strictEqual(findeErstesVielfaches([10, 20, 30], 5), 10, "Erstes Vielfaches von 5 in [10, 20, 30] sollte 10 sein");
|
||||||
|
|
||||||
|
console.log("✅ Alle 28 JavaScript Tests in JS 02 erfolgreich bestanden!");
|
||||||
173
courses/javascript/03_funktionen_und_scope/README.md
Normal file
173
courses/javascript/03_funktionen_und_scope/README.md
Normal file
|
|
@ -0,0 +1,173 @@
|
||||||
|
# JS 03: Funktionen, Gültigkeitsbereiche (Scope) & Closures 🔐
|
||||||
|
|
||||||
|
Willkommen zu **Modul 03** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
In JavaScript sind Funktionen sogenannte **First-Class Citizens**: Sie können wie gewöhnliche Variablen gespeichert, als Argumente übergeben und von anderen Funktionen zurückgegeben werden (*Higher-Order Functions*).
|
||||||
|
|
||||||
|
In diesem Modul meisterst du moderne Funktionsarten, Gültigkeitsbereiche (*Scopes*) und eines der faszinierendsten Kernkonzepte von JavaScript: **Closures**!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. Funktionsarten im Vergleich
|
||||||
|
|
||||||
|
#### 1. Function Declaration (Klassische Funktionsdeklaration)
|
||||||
|
Wird vollständig gehoistet (kann bereits vor ihrer Definition im Code aufgerufen werden):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
console.log(quadriere(4)); // 16 (funktioniert dank Hoisting!)
|
||||||
|
|
||||||
|
function quadriere(x) {
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Function Expression (Funktionsausdruck)
|
||||||
|
Wird einer Variablen zugewiesen und ist erst ab der Zuweisungszeile verfügbar:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const verdopple = function(x) {
|
||||||
|
return x * 2;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. Arrow Function (Pfeilfunktion / ES6)
|
||||||
|
Kompakte Syntax, kein eigenes `this` oder `arguments`-Objekt:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Ausführliche Arrow Function:
|
||||||
|
const addiere = (a, b) => {
|
||||||
|
return a + b;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Einzeilige Arrow Function mit implizitem Return:
|
||||||
|
const multipliziere = (a, b) => a * b;
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Moderne Parameter: Default- & Rest-Parameter
|
||||||
|
|
||||||
|
#### 1. Default-Parameter (Standardwerte)
|
||||||
|
Verhindert `undefined`, wenn kein Wert übergeben wird:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function begruesse(name = "Gast", sprache = "de") {
|
||||||
|
return sprache === "de" ? `Hallo, ${name}!` : `Hello, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(begruesse()); // "Hallo, Gast!"
|
||||||
|
console.log(begruesse("Anna", "en")); // "Hello, Anna!"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. Rest-Parameter (`...rest`)
|
||||||
|
Sammelt eine beliebige Anzahl von Argumenten in einem echten JavaScript-Array:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function maxZahl(...zahlen) {
|
||||||
|
// 'zahlen' ist ein echtes Array!
|
||||||
|
return Math.max(...zahlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(maxZahl(3, 9, 21, 4)); // 21
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Gültigkeitsbereiche (Scopes) & Hoisting
|
||||||
|
|
||||||
|
JavaScript kennt drei Ebenen von Scopes:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ 🌍 GLOBAL SCOPE (Überall im Programm sichtbar) │
|
||||||
|
│ const globalVar = "Welt"; │
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────┐ │
|
||||||
|
│ │ 📦 FUNCTION SCOPE (Nur innerhalb der Funktion) │ │
|
||||||
|
│ │ function meineFunktion() { │ │
|
||||||
|
│ │ const funcVar = "Funktion"; │ │
|
||||||
|
│ │ │ │
|
||||||
|
│ │ ┌───────────────────────────────────────┐ │ │
|
||||||
|
│ │ │ 🔒 BLOCK SCOPE (if, for, { let/const})│ │ │
|
||||||
|
│ │ │ if (true) { │ │ │
|
||||||
|
│ │ │ const blockVar = "Block"; │ │ │
|
||||||
|
│ │ │ } │ │ │
|
||||||
|
│ │ └───────────────────────────────────────┘ │ │
|
||||||
|
│ │ } │ │
|
||||||
|
│ └─────────────────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`let` und `const`** besitzen **Block-Scope** (gelten nur innerhalb der `{ ... }` geschweiften Klammern).
|
||||||
|
- **`var`** besitzt nur **Function-Scope** (ignoriert `if`- und `for`-Blöcke) und sollte in modernem JS vermieden werden.
|
||||||
|
- **Lexical Scope (Statische Bindung):** Innere Funktionen können auf Variablen ihrer äußeren Eltern-Scopes zugreifen, aber nicht umgekehrt!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D. Closures: Das Super-Feature
|
||||||
|
|
||||||
|
> **Definition Closure:** Eine Closure entsteht, wenn eine innere Funktion Zugriff auf Variablen ihres äußeren Gültigkeitsbereichs behält – **selbst nachdem die äußere Funktion ihre Ausführung längst beendet hat**.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function erstelleSparschwein() {
|
||||||
|
let guthaben = 0; // Private Variable, von außen nicht direkt änderbar!
|
||||||
|
|
||||||
|
return function(einzahlung) {
|
||||||
|
guthaben += einzahlung;
|
||||||
|
return `Aktuelles Guthaben: ${guthaben} €`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const meinKonto = erstelleSparschwein();
|
||||||
|
console.log(meinKonto(50)); // "Aktuelles Guthaben: 50 €"
|
||||||
|
console.log(meinKonto(100)); // "Aktuelles Guthaben: 150 €"
|
||||||
|
|
||||||
|
// Wichtig: 'guthaben' kann von außen nicht manipuliert werden (z.B. kein meinKonto.guthaben = 0)!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Die didaktische Analogie: "Der Tresor mit Geheimzahl"
|
||||||
|
|
||||||
|
Stelle dir eine Closure wie einen **verschlossenen Tresor mit einem Einwurf-Schlitz und Zahlenrad** vor:
|
||||||
|
|
||||||
|
- **Der Erbauer (Die äußere Factory-Funktion):**
|
||||||
|
Die äußere Funktion baut den Tresor und legt die interne Mechanik (die geschützten Variablen `let zaehler`) im Inneren des Gehäuses an.
|
||||||
|
- **Der Schlüsselanhänger (Die zurückgegebene innere Funktion):**
|
||||||
|
Du erhältst von der Funktion lediglich den Schlüssel (die Referenz auf die innere Funktion).
|
||||||
|
- **Geschützte Kapselung (Data Encapsulation):**
|
||||||
|
Niemand von außen kann die Tresorwand aufbrechen oder direkt an die internen Zahnräder fassen. Nur wenn du den Schlüssel drehst (`zaehler()`), verändert sich der interne Zählerstand kontrolliert.
|
||||||
|
- **Unabhängige Instanzen:**
|
||||||
|
Jeder neue Aufruf der äußeren Funktion baut einen komplett eigenständigen Tresor. Tresor A und Tresor B beeinflussen sich gegenseitig zu keinem Zeitpunkt!
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
Öffne die Datei `aufgabe.js` und implementiere folgende 4 Funktionen:
|
||||||
|
|
||||||
|
1. **TODO 1: `erstelleZaehler(startWert = 0)`**
|
||||||
|
Gibt eine Zählerfunktion (Closure) zurück. Jeder Aufruf erhöht den internen Zähler um 1 und gibt den neuen Stand zurück.
|
||||||
|
|
||||||
|
2. **TODO 2: `multiplizierer(faktor)`**
|
||||||
|
Erzeugt eine Multiplikator-Funktion (Closure): `(x) => x * faktor`.
|
||||||
|
*Beispiel:* `const verdopple = multiplizierer(2); verdopple(5); // 10`
|
||||||
|
|
||||||
|
3. **TODO 3: `summiereAlles(...zahlen)`**
|
||||||
|
Nimmt beliebig viele Zahlen per Rest-Parameter entgegen und gibt deren Gesamtsumme zurück. Bei leerem Aufruf wird `0` zurückgegeben.
|
||||||
|
|
||||||
|
4. **TODO 4: `erstellePraefixierer(praefix)`**
|
||||||
|
Gibt eine Funktion (Closure) zurück, die vor jeden übergebenen String das vorgegebene `praefix` anhängt: `(text) => `${praefix}${text}``.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE oder führe die Testsuite aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Im Browser:
|
||||||
|
workspace.html?course=javascript&track=track_1_grundlagen&chapter=03_funktionen_und_scope
|
||||||
|
```
|
||||||
60
courses/javascript/03_funktionen_und_scope/aufgabe.js
Normal file
60
courses/javascript/03_funktionen_und_scope/aufgabe.js
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 03: FUNKTIONEN, SCOPE & CLOSURES 🌐
|
||||||
|
* ==========================================
|
||||||
|
* Funktionsarten, Gültigkeitsbereiche, Rest-Parameter und Closures.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): erstelleZaehler(startWert = 0)
|
||||||
|
// Erstelle eine Funktion, die einen internen Zähler kapselt (Closure).
|
||||||
|
// Die zurückgegebene Funktion soll bei jedem Aufruf den Zähler um 1 erhöhen
|
||||||
|
// und den neuen Wert zurückgeben.
|
||||||
|
// Standardwert für startWert ist 0.
|
||||||
|
// Beispiel:
|
||||||
|
// const zaehler = erstelleZaehler(0);
|
||||||
|
// zaehler(); // 1
|
||||||
|
// zaehler(); // 2
|
||||||
|
function erstelleZaehler(startWert = 0) {
|
||||||
|
// TODO: Innere Zählerfunktion (Closure) zurückgeben
|
||||||
|
return function() {
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): multiplizierer(faktor)
|
||||||
|
// Erstelle eine Factory-Funktion (Closure), die eine Multiplikationsfunktion zurückgibt.
|
||||||
|
// Die zurückgegebene Funktion nimmt eine Zahl x entgegen und gibt (x * faktor) zurück.
|
||||||
|
// Beispiel:
|
||||||
|
// const verdopple = multiplizierer(2);
|
||||||
|
// verdopple(5); // 10
|
||||||
|
// const verdreifache = multiplizierer(3);
|
||||||
|
// verdreifache(4); // 12
|
||||||
|
function multiplizierer(faktor) {
|
||||||
|
// TODO: Arrow-Funktion oder anonyme Funktion zurückgeben
|
||||||
|
return (x) => 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): summiereAlles(...zahlen)
|
||||||
|
// Berechne die Summe beliebig vieler übergebener Zahlen unter Verwendung des
|
||||||
|
// Rest-Parameters (...zahlen).
|
||||||
|
// Werden keine Argumente übergeben, soll 0 zurückgegeben werden.
|
||||||
|
// Beispiel:
|
||||||
|
// summiereAlles(1, 2, 3, 4); // 10
|
||||||
|
// summiereAlles(5, -2); // 3
|
||||||
|
// summiereAlles(); // 0
|
||||||
|
function summiereAlles(...zahlen) {
|
||||||
|
// TODO: Rest-Parameter aufsummieren
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): erstellePraefixierer(praefix)
|
||||||
|
// Erstelle eine Funktion (Closure), die einen String als Präfix vor jeden
|
||||||
|
// übergebenen Text setzt.
|
||||||
|
// Beispiel:
|
||||||
|
// const mitEuro = erstellePraefixierer("€ ");
|
||||||
|
// mitEuro("100"); // "€ 100"
|
||||||
|
// const https = erstellePraefixierer("https://");
|
||||||
|
// https("google.com"); // "https://google.com"
|
||||||
|
function erstellePraefixierer(praefix) {
|
||||||
|
// TODO: Closure-Funktion zurückgeben
|
||||||
|
return (text) => "";
|
||||||
|
}
|
||||||
167
courses/javascript/03_funktionen_und_scope/index.html
Normal file
167
courses/javascript/03_funktionen_und_scope/index.html
Normal file
|
|
@ -0,0 +1,167 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 03: Funktionen, Scope & Closures</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
||||||
|
<h1>JS 03: Funktionen, Scope & Closures</h1>
|
||||||
|
<p>Meistere moderne Funktionsdeklarationen, Arrow Functions, Rest-Parameter, Gültigkeitsbereiche und Closures.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: FUNKTIONSARTEN & PARAMETER -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Funktionsarten & Moderne Parameter</h2>
|
||||||
|
<p>JavaScript bietet klassische Deklarationen, Funktionsausdrücke und moderne <strong>Arrow Functions</strong>. Mit <strong>Default-</strong> und <strong>Rest-Parametern</strong> schreibst du flexiblen Code:</p>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 1. Arrow Function mit implizitem Return:
|
||||||
|
const verdopple = x => x * 2;
|
||||||
|
|
||||||
|
// 🎯 2. Default-Parameter:
|
||||||
|
function begruesse(name = "Gast") {
|
||||||
|
return `Willkommen, ${name}!`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 3. Rest-Parameter (...sammelt alle Argumente als Array):
|
||||||
|
function summiere(...zahlen) {
|
||||||
|
return zahlen.reduce((sum, n) => sum + n, 0);
|
||||||
|
}
|
||||||
|
console.log(summiere(10, 20, 30)); // 60</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: SCOPE & HOISTING -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🌍 2. Gültigkeitsbereiche (Scope Hierarchy)</h2>
|
||||||
|
<p>Variablen sind immer nur in ihrem deklarierten Bereich gültig. <code>let</code> und <code>const</code> erzeugen echte <strong>Block-Scopes</strong>:</p>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-subtle); padding: 18px; border-radius: var(--radius-sm); font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.5; margin-bottom: 16px; border: 1px solid var(--border-color);">
|
||||||
|
🌍 <strong>Global Scope</strong>: Überall im Skript verfügbar<br>
|
||||||
|
└── 📦 <strong>Function Scope</strong>: Nur innerhalb der Funktion sichtbar<br>
|
||||||
|
└── 🔒 <strong>Block Scope</strong> (<code>{ let x = 1; }</code>): Nur innerhalb des <code>{ }</code>-Blocks
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code>function scopeTest() {
|
||||||
|
const inside = "Ich lebe nur in der Funktion";
|
||||||
|
if (true) {
|
||||||
|
const block = "Ich lebe nur im if-Block";
|
||||||
|
}
|
||||||
|
// console.log(block); // ❌ ReferenceError: block is not defined
|
||||||
|
}</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: CLOSURES -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔐 3. Closures: Funktionen mit Gedächtnis</h2>
|
||||||
|
<p>Eine <strong>Closure</strong> entsteht, wenn eine innere Funktion auf Variablen ihrer umgebenden Funktion zugreift – auch nachdem die äußere Funktion beendet ist:</p>
|
||||||
|
|
||||||
|
<pre><code>function erstelleZaehler(start = 0) {
|
||||||
|
let counter = start; // Kapselung (private Variable)
|
||||||
|
|
||||||
|
return function() {
|
||||||
|
counter++;
|
||||||
|
return counter;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const zaehlerA = erstelleZaehler(0);
|
||||||
|
console.log(zaehlerA()); // 1
|
||||||
|
console.log(zaehlerA()); // 2
|
||||||
|
|
||||||
|
const zaehlerB = erstelleZaehler(100);
|
||||||
|
console.log(zaehlerB()); // 101 (Komplett unabhängig von zaehlerA!)</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🔐 Didaktische Analogie: Der Tresor mit Geheimzahl</div>
|
||||||
|
<p>Die äußere Funktion ist der <strong>Tresorbauer</strong>: Sie konstruiert ein geschütztes Gehäuse (die privaten Variablen) und händigt dir lediglich einen <strong>Schlüsselanhänger</strong> (die zurückgegebene innere Funktion) aus.</p>
|
||||||
|
<p>Niemand kann von außen in den Tresor greifen – nur durch Drehen des Schlüssels (Funktionsaufruf) wird das interne Zahnrad weitergedreht. Jeder neue Aufruf der äußeren Funktion baut einen neuen, isolierten Tresor!</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: Funktionen & Scopes</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Konzept</th>
|
||||||
|
<th style="padding: 10px;">Syntax / Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Besonderheit</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Arrow Function</code></td>
|
||||||
|
<td style="padding: 10px;"><code>const f = (x) => x * 2;</code></td>
|
||||||
|
<td style="padding: 10px;">Kompakt, lexikalisches <code>this</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Default-Parameter</code></td>
|
||||||
|
<td style="padding: 10px;"><code>function f(a = 10) { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Greift bei fehlendem Argument / <code>undefined</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Rest-Parameter</code></td>
|
||||||
|
<td style="padding: 10px;"><code>function f(...args) { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Sammelt alle Parameter in ein Array</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Block Scope</code></td>
|
||||||
|
<td style="padding: 10px;"><code>{ const x = 42; }</code></td>
|
||||||
|
<td style="padding: 10px;">Gültigkeit beschränkt auf geschweifte Klammern</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>Closure</code></td>
|
||||||
|
<td style="padding: 10px;"><code>return () => ++counter;</code></td>
|
||||||
|
<td style="padding: 10px;">Behält Zugriff auf Variablen des Erzeugungs-Scopes</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>erstelleZaehler(startWert = 0)</code> als Closure bauen (inkrementiert bei jedem Aufruf).</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>multiplizierer(faktor)</code> als Higher-Order Function implementieren.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>summiereAlles(...zahlen)</code> mit Rest-Parameter und Summenberechnung schreiben.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>erstellePraefixierer(praefix)</code> zur String-Präfixierung als Closure erstellen.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 1 & 4</div>
|
||||||
|
<p>Für <code>erstelleZaehler</code> deklariere <code>let zaehler = startWert;</code> in der äußeren Funktion und gib eine innere Funktion zurück, die <code>zaehler++</code> rechnet und den neuen Wert zurückliefert!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=03_funktionen_und_scope" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../02_kontrollfluss_und_schleifen/index.html" class="btn btn-secondary">← Zurück zu JS 02</a>
|
||||||
|
<a href="../04_arrays_sets_und_maps/index.html" class="btn btn-secondary">Weiter zu JS 04 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
19
courses/javascript/03_funktionen_und_scope/musterloesung.js
Normal file
19
courses/javascript/03_funktionen_und_scope/musterloesung.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
function erstelleZaehler(startWert = 0) {
|
||||||
|
let zaehler = startWert;
|
||||||
|
return function() {
|
||||||
|
zaehler++;
|
||||||
|
return zaehler;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function multiplizierer(faktor) {
|
||||||
|
return (x) => x * faktor;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summiereAlles(...zahlen) {
|
||||||
|
return zahlen.reduce((sum, n) => sum + n, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function erstellePraefixierer(praefix) {
|
||||||
|
return (text) => `${praefix}${text}`;
|
||||||
|
}
|
||||||
44
courses/javascript/03_funktionen_und_scope/test_aufgabe.js
Normal file
44
courses/javascript/03_funktionen_und_scope/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// 🧪 Testsuite: JS 03 - Funktionen, Scope & Closures
|
||||||
|
|
||||||
|
// Test 1: erstelleZaehler (Standardwert 0)
|
||||||
|
const z1 = erstelleZaehler();
|
||||||
|
assert.strictEqual(z1(), 1, "Erster Aufruf von erstelleZaehler() sollte 1 zurückgeben");
|
||||||
|
assert.strictEqual(z1(), 2, "Zweiter Aufruf von erstelleZaehler() sollte 2 zurückgeben");
|
||||||
|
assert.strictEqual(z1(), 3, "Dritter Aufruf von erstelleZaehler() sollte 3 zurückgeben");
|
||||||
|
|
||||||
|
// Test 1b: erstelleZaehler mit Startwert 10
|
||||||
|
const z2 = erstelleZaehler(10);
|
||||||
|
assert.strictEqual(z2(), 11, "Erster Aufruf von erstelleZaehler(10) sollte 11 zurückgeben");
|
||||||
|
assert.strictEqual(z2(), 12, "Zweiter Aufruf von erstelleZaehler(10) sollte 12 zurückgeben");
|
||||||
|
|
||||||
|
// Test 1c: Closure-Isolation (z1 und z2 dürfen sich nicht gegenseitig beeinflussen)
|
||||||
|
assert.strictEqual(z1(), 4, "z1 sollte unabhängig von z2 weiterzählen (erwartet: 4)");
|
||||||
|
|
||||||
|
// Test 2: multiplizierer
|
||||||
|
const verdopple = multiplizierer(2);
|
||||||
|
assert.strictEqual(verdopple(5), 10, "verdopple(5) sollte 10 sein");
|
||||||
|
assert.strictEqual(verdopple(0), 0, "verdopple(0) sollte 0 sein");
|
||||||
|
assert.strictEqual(verdopple(-4), -8, "verdopple(-4) sollte -8 sein");
|
||||||
|
|
||||||
|
const verdreifache = multiplizierer(3);
|
||||||
|
assert.strictEqual(verdreifache(4), 12, "verdreifache(4) sollte 12 sein");
|
||||||
|
|
||||||
|
const halbiere = multiplizierer(0.5);
|
||||||
|
assert.strictEqual(halbiere(20), 10, "halbiere(20) sollte 10 sein");
|
||||||
|
|
||||||
|
// Test 3: summiereAlles (Rest-Parameter)
|
||||||
|
assert.strictEqual(summiereAlles(), 0, "summiereAlles() ohne Argumente sollte 0 sein");
|
||||||
|
assert.strictEqual(summiereAlles(42), 42, "summiereAlles(42) sollte 42 sein");
|
||||||
|
assert.strictEqual(summiereAlles(1, 2, 3, 4, 5), 15, "summiereAlles(1, 2, 3, 4, 5) sollte 15 sein");
|
||||||
|
assert.strictEqual(summiereAlles(10, -5, 3), 8, "summiereAlles(10, -5, 3) sollte 8 sein");
|
||||||
|
|
||||||
|
// Test 4: erstellePraefixierer
|
||||||
|
const mitEuro = erstellePraefixierer("€ ");
|
||||||
|
assert.strictEqual(mitEuro("100"), "€ 100", "mitEuro('100') sollte '€ 100' sein");
|
||||||
|
assert.strictEqual(mitEuro("0"), "€ 0", "mitEuro('0') sollte '€ 0' sein");
|
||||||
|
|
||||||
|
const mitHttps = erstellePraefixierer("https://");
|
||||||
|
assert.strictEqual(mitHttps("github.com"), "https://github.com", "mitHttps('github.com') sollte 'https://github.com' sein");
|
||||||
|
assert.strictEqual(mitHttps("developer.mozilla.org"), "https://developer.mozilla.org", "mitHttps('developer.mozilla.org') sollte 'https://developer.mozilla.org' sein");
|
||||||
|
|
||||||
|
console.log("✅ Alle 18 JavaScript Tests in JS 03 erfolgreich bestanden!");
|
||||||
123
courses/javascript/04_arrays_sets_und_maps/README.md
Normal file
123
courses/javascript/04_arrays_sets_und_maps/README.md
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
# JS 04: Arrays, Sets & Maps 📊
|
||||||
|
|
||||||
|
Willkommen zu **Modul 04** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
In modernen Web-Anwendungen sind saubere Datenstrukturen das A und O. JavaScript stellt dir drei mächtige Sammlungen zur Verfügung:
|
||||||
|
1. **Arrays:** Geordnete, indexierte Listen mit funktionalen Methoden wie `.map()`, `.filter()`, `.reduce()`.
|
||||||
|
2. **Sets:** Sammlungen einzigartiger Werte – doppelte Einträge werden automatisch eliminiert.
|
||||||
|
3. **Maps:** Flexible Schlüssel-Wert-Speicher, die im Gegensatz zu normalen Objekten jeden Datentyp als Schlüssel erlauben.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. Funktionale Array-Methoden (ES6+)
|
||||||
|
|
||||||
|
Statt schwerfälliger `for`-Schleifen transformieren wir Arrays deklarativ und ohne Seiteneffekte:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const zahlen = [1, 2, 3, 4, 5];
|
||||||
|
|
||||||
|
// 🎯 1. .map(): Jedes Element eins-zu-eins transformieren
|
||||||
|
const verdoppelt = zahlen.map(x => x * 2); // [2, 4, 6, 8, 10]
|
||||||
|
|
||||||
|
// 🎯 2. .filter(): Elemente nach einer Bedingung filtern
|
||||||
|
const gerade = zahlen.filter(x => x % 2 === 0); // [2, 4]
|
||||||
|
|
||||||
|
// 🎯 3. .reduce(): Das gesamte Array zu einem Einzelwert aggregieren
|
||||||
|
const summe = zahlen.reduce((akkumulator, aktuellerWert) => akkumulator + aktuellerWert, 0); // 15
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Sets: Mengen ohne Duplikate
|
||||||
|
|
||||||
|
Ein `Set` speichert ausschließlich **eindeutige Werte**. Versuche, denselben Wert mehrfach hinzuzufügen, werden ignoriert:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Set erstellen:
|
||||||
|
const farben = new Set(["rot", "gruen", "rot", "blau"]);
|
||||||
|
console.log(farben.size); // 3 (da "rot" nur 1x gezählt wird)
|
||||||
|
|
||||||
|
// Werte hinzufügen & prüfen:
|
||||||
|
farben.add("gelb");
|
||||||
|
console.log(farben.has("rot")); // true (extrem schnelle O(1) Prüfung!)
|
||||||
|
farben.delete("blau");
|
||||||
|
|
||||||
|
// 🌟 Duplikate aus einem Array in 1 Zeile entfernen:
|
||||||
|
const mitDuplikaten = [1, 2, 2, 3, 3, 3, 4];
|
||||||
|
const ohneDuplikate = [...new Set(mitDuplikaten)]; // [1, 2, 3, 4]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Maps: Schlüssel-Wert-Speicher (Key-Value)
|
||||||
|
|
||||||
|
Eine `Map` speichert Schlüssel-Wert-Paare in Einfügereihenfolge. Im Gegensatz zu einfachen `{}` Objekten darf ein Schlüssel in einer Map **jeder beliebige Typ** sein (auch Objekte, Zahlen oder Booleans):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Map instanziieren:
|
||||||
|
const benutzerStatus = new Map();
|
||||||
|
|
||||||
|
// Einträge setzen & abrufen:
|
||||||
|
benutzerStatus.set("user_101", { online: true, rolle: "Admin" });
|
||||||
|
benutzerStatus.set("user_102", { online: false, rolle: "Gast" });
|
||||||
|
|
||||||
|
console.log(benutzerStatus.get("user_101")); // { online: true, rolle: "Admin" }
|
||||||
|
console.log(benutzerStatus.has("user_999")); // false
|
||||||
|
console.log(benutzerStatus.size); // 2
|
||||||
|
|
||||||
|
// Aus 2D-Array initialisieren:
|
||||||
|
const uebersetzung = new Map([
|
||||||
|
["de", "Guten Tag"],
|
||||||
|
["en", "Good day"],
|
||||||
|
["fr", "Bonjour"]
|
||||||
|
]);
|
||||||
|
console.log(uebersetzung.get("en")); // "Good day"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 Die didaktische Analogie: "Fotogalerie, VIP-Club & Telefonbuch"
|
||||||
|
|
||||||
|
- 📸 **Das Array (Die Fotogalerie):**
|
||||||
|
Wie ein Fotoalbum: Jedes Foto liegt an einer festen Position (`index 0, 1, 2...`). Du kannst dasselbe Bild mehrfach einkleben. Mit Filtern legst du Schablonen über alle Bilder.
|
||||||
|
|
||||||
|
- 🎟️ **Das Set (Der VIP-Club):**
|
||||||
|
Der Türsteher führt eine Gästeliste. Niemand kommt doppelt auf die Liste. Wenn jemand bereits drin ist (`set.has("Max")`), wird ein zweiter Einlassversuch ignoriert.
|
||||||
|
|
||||||
|
- 📖 **Die Map (Das schlaue Telefonbuch):**
|
||||||
|
Zu jedem Namen (Schlüssel) gibt es den passenden Eintrag (Wert). Du schlägst blitzschnell den Namen nach (`map.get("Anna")`) und erhältst direkt die hinterlegte Information.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1: `verdoppeln(zahlen)`**
|
||||||
|
Gibt ein neues Array zurück, in dem alle Zahlen mit `.map()` verdoppelt wurden.
|
||||||
|
|
||||||
|
2. **TODO 2: `filterGeradeZahlen(zahlen)`**
|
||||||
|
Filtert mit `.filter()` nur die geraden Zahlen heraus.
|
||||||
|
|
||||||
|
3. **TODO 3: `arraySumme(zahlen)`**
|
||||||
|
Berechnet die Summe aller Array-Elemente mit `.reduce()` (Standardwert `0`).
|
||||||
|
|
||||||
|
4. **TODO 4: `entferneDuplikate(liste)`**
|
||||||
|
Eliminiert alle Duplikate aus dem übergebenen Array mithilfe von `new Set()` und gibt ein bereinigtes Array zurück.
|
||||||
|
|
||||||
|
5. **TODO 5: `erstelleWoerterbuch(paare)`**
|
||||||
|
Erzeugt eine neue JavaScript `Map` aus dem übergebenen 2D-Array von Schlüssel-Wert-Paaren (`[[k, v], ...]`).
|
||||||
|
|
||||||
|
6. **TODO 6: `zaehleHaeufigkeiten(woerter)`**
|
||||||
|
Zählt die Häufigkeit jedes Wortes im Array und gibt eine `Map` zurück, die jedes Wort auf seine Anzahl abbildet.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE oder führe die Tests aus:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Im Browser:
|
||||||
|
workspace.html?course=javascript&track=track_1_grundlagen&chapter=04_arrays_sets_und_maps
|
||||||
|
```
|
||||||
54
courses/javascript/04_arrays_sets_und_maps/aufgabe.js
Normal file
54
courses/javascript/04_arrays_sets_und_maps/aufgabe.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 04: ARRAYS, SETS & MAPS 🌐
|
||||||
|
* =================================
|
||||||
|
* Funktionale Datenstrukturen, Mengen ohne Duplikate (Set) und Schlüssel-Wert-Speicher (Map).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): verdoppeln(zahlen)
|
||||||
|
// Erstelle ein neues Array, in dem alle Zahlen verdoppelt sind (unter Nutzung von .map()).
|
||||||
|
function verdoppeln(zahlen) {
|
||||||
|
// TODO: Array mit .map() transformieren
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): filterGeradeZahlen(zahlen)
|
||||||
|
// Filtere nur die geraden Zahlen aus dem Array heraus (unter Nutzung von .filter()).
|
||||||
|
function filterGeradeZahlen(zahlen) {
|
||||||
|
// TODO: Nur gerade Zahlen mit .filter() beibehalten
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): arraySumme(zahlen)
|
||||||
|
// Berechne die Gesamtsumme aller Zahlen im Array mit .reduce().
|
||||||
|
// Bei leerem Array soll 0 zurückgegeben werden.
|
||||||
|
function arraySumme(zahlen) {
|
||||||
|
// TODO: Summe mit .reduce() aggregieren
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): entferneDuplikate(liste)
|
||||||
|
// Entferne alle doppelten Werte aus dem Array 'liste' mithilfe einer Set-Datenstruktur (new Set()).
|
||||||
|
// Gib das Ergebnis als echtes Array zurück (z.B. mit dem Spread-Operator [...new Set(liste)]).
|
||||||
|
// Beispiel: entferneDuplikate([1, 2, 2, 3, 1, 4]) -> [1, 2, 3, 4]
|
||||||
|
function entferneDuplikate(liste) {
|
||||||
|
// TODO: new Set() nutzen und als Array zurückgeben
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 5 (TODO 5): erstelleWoerterbuch(paare)
|
||||||
|
// Erstelle aus einem zweidimensionalen Array von Schlüssel-Wert-Paaren eine JavaScript Map (new Map()).
|
||||||
|
// Beispiel: erstelleWoerterbuch([["de", "Hallo"], ["en", "Hello"]]) -> Map { 'de' => 'Hallo', 'en' => 'Hello' }
|
||||||
|
function erstelleWoerterbuch(paare) {
|
||||||
|
// TODO: new Map(paare) erstellen und zurückgeben
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 6 (TODO 6): zaehleHaeufigkeiten(woerter)
|
||||||
|
// Zähle, wie oft jedes Wort im übergebenen Array 'woerter' vorkommt.
|
||||||
|
// Nutze eine Map, um die Zählerstände zu verwalten (map.set(wort, (map.get(wort) || 0) + 1)).
|
||||||
|
// Gib die fertige Map zurück.
|
||||||
|
// Beispiel: zaehleHaeufigkeiten(["apfel", "banane", "apfel"]) -> Map { 'apfel' => 2, 'banane' => 1 }
|
||||||
|
function zaehleHaeufigkeiten(woerter) {
|
||||||
|
// TODO: Map instanziieren, Wörter durchlaufen und Häufigkeit zählen
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
179
courses/javascript/04_arrays_sets_und_maps/index.html
Normal file
179
courses/javascript/04_arrays_sets_und_maps/index.html
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 04: Arrays, Sets & Maps</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 JavaScript Grundlagen</div>
|
||||||
|
<h1>JS 04: Arrays, Sets & Maps</h1>
|
||||||
|
<p>Lerne funktionale Transformationen mit <code>.map()</code>, <code>.filter()</code>, <code>.reduce()</code> sowie einzigartige Sets und flexible Maps.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: ARRAY METHODEN -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>💡 1. Moderne Array-Methoden (<code>map</code>, <code>filter</code>, <code>reduce</code>)</h2>
|
||||||
|
<p>Verarbeite Datenreihen deklarativ, lesbar und unveränderlich (ohne das Original-Array zu mutieren):</p>
|
||||||
|
|
||||||
|
<pre><code>const preise = [10, 25, 40, 5];
|
||||||
|
|
||||||
|
// 🎯 1. .map() - Jedes Element transformieren:
|
||||||
|
const mitMwSt = preise.map(p => p * 1.19);
|
||||||
|
|
||||||
|
// 🎯 2. .filter() - Elemente nach Kriterium filtern:
|
||||||
|
const teuer = preise.filter(p => p >= 20); // [25, 40]
|
||||||
|
|
||||||
|
// 🎯 3. .reduce() - Aggregieren zu einem Gesamtwert:
|
||||||
|
const gesamt = preise.reduce((sum, p) => sum + p, 0); // 80</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: SETS -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎟️ 2. Sets: Eindeutige Mengen ohne Duplikate</h2>
|
||||||
|
<p>Ein <code>Set</code> garantiert, dass jeder Wert nur genau einmal vorkommt. Ideal für Duplikatbereinigung und schnelle Existenzprüfungen:</p>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 Set erstellen & befüllen:
|
||||||
|
const tags = new Set(["js", "web", "js", "frontend"]);
|
||||||
|
console.log(tags.size); // 3 (da "js" nur 1x existiert)
|
||||||
|
|
||||||
|
tags.add("backend");
|
||||||
|
console.log(tags.has("web")); // true (O(1) schneller Lookup)
|
||||||
|
|
||||||
|
// 🌟 Duplikate aus Array entfernen (Spread):
|
||||||
|
const duplikate = [1, 2, 2, 3, 1, 4];
|
||||||
|
const einzigartig = [...new Set(duplikate)]; // [1, 2, 3, 4]</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: MAPS -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📖 3. Maps: Flexible Schlüssel-Wert-Speicher</h2>
|
||||||
|
<p>Eine <code>Map</code> verknüpft Schlüssel mit Werten. Jeder Schlüssel kann einen beliebigen Typ haben (auch Strings, Zahlen oder Objekte):</p>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 Map erstellen & befüllen:
|
||||||
|
const config = new Map();
|
||||||
|
config.set("thema", "dark");
|
||||||
|
config.set("benachrichtigungen", true);
|
||||||
|
|
||||||
|
console.log(config.get("thema")); // "dark"
|
||||||
|
console.log(config.has("sprache")); // false
|
||||||
|
console.log(config.size); // 2
|
||||||
|
|
||||||
|
// 🎯 Aus 2D-Array initialisieren:
|
||||||
|
const lexikon = new Map([
|
||||||
|
["de", "Guten Tag"],
|
||||||
|
["en", "Hello"]
|
||||||
|
]);</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🎭 Didaktische Analogie: Fotogalerie, VIP-Club & Telefonbuch</div>
|
||||||
|
<p><strong>Array (Die Fotogalerie):</strong> Ein Fotoalbum mit festen Seiten-Nummern (Indizes 0, 1, 2...). Du kannst dasselbe Bild mehrfach einkleben und Filterfolien darüber legen.</p>
|
||||||
|
<p><strong>Set (Der VIP-Club):</strong> Die Gästeliste an der Tür: Ein Gast kann nicht zweimal auf der Liste stehen. Neue Duplikate werden sofort abgewiesen.</p>
|
||||||
|
<p><strong>Map (Das moderne Telefonbuch):</strong> Schnelles Nachschlagen: Du nennst den Namen (Key) und bekommst sofort die Nummer (Value) zurück.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: Array, Set & Map</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Typ</th>
|
||||||
|
<th style="padding: 10px;">Syntax / Methode</th>
|
||||||
|
<th style="padding: 10px;">Beschreibung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Array</code></td>
|
||||||
|
<td style="padding: 10px;"><code>arr.map(fn)</code></td>
|
||||||
|
<td style="padding: 10px;">Erzeugt neues transformiertes Array</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Array</code></td>
|
||||||
|
<td style="padding: 10px;"><code>arr.filter(fn)</code></td>
|
||||||
|
<td style="padding: 10px;">Filtert Elemente nach Wahrheitsbedingung</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Array</code></td>
|
||||||
|
<td style="padding: 10px;"><code>arr.reduce(fn, start)</code></td>
|
||||||
|
<td style="padding: 10px;">Aggregiert alle Werte zu einem Gesamtwert</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Set</code></td>
|
||||||
|
<td style="padding: 10px;"><code>new Set(arr) / [...set]</code></td>
|
||||||
|
<td style="padding: 10px;">Menge ohne Duplikate; Konvertierung zu Array</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Set</code></td>
|
||||||
|
<td style="padding: 10px;"><code>set.has(val) / set.add(val)</code></td>
|
||||||
|
<td style="padding: 10px;">Element prüfen / hinzufügen (Größe: <code>.size</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Map</code></td>
|
||||||
|
<td style="padding: 10px;"><code>map.set(k, v) / map.get(k)</code></td>
|
||||||
|
<td style="padding: 10px;">Schlüssel-Wert-Paar setzen / abrufen</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>Map</code></td>
|
||||||
|
<td style="padding: 10px;"><code>new Map(entries)</code></td>
|
||||||
|
<td style="padding: 10px;">Initialisierung aus <code>[[k, v], ...]</code></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>verdoppeln(zahlen)</code> mit <code>.map()</code> umsetzen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>filterGeradeZahlen(zahlen)</code> mit <code>.filter()</code> filtern.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>arraySumme(zahlen)</code> mit <code>.reduce()</code> berechnen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>entferneDuplikate(liste)</code> mit <code>new Set()</code> und Spread-Operator schreiben.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t5">
|
||||||
|
<label class="task-label" for="t5"><strong>TODO 5:</strong> <code>erstelleWoerterbuch(paare)</code> mit <code>new Map(paare)</code> initialisieren.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t6">
|
||||||
|
<label class="task-label" for="t6"><strong>TODO 6:</strong> <code>zaehleHaeufigkeiten(woerter)</code> mit <code>Map</code>-Zähler aufbauen.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 4 & 6</div>
|
||||||
|
<p>Für <code>entferneDuplikate</code>: <code>return [...new Set(liste)];</code>. Für <code>zaehleHaeufigkeiten</code>: Iteriere mit <code>for (const w of woerter)</code> und setze <code>map.set(w, (map.get(w) || 0) + 1);</code>.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=04_arrays_sets_und_maps" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../03_funktionen_und_scope/index.html" class="btn btn-secondary">← Zurück zu JS 03</a>
|
||||||
|
<a href="../05_objekte_und_destructuring/index.html" class="btn btn-secondary">Weiter zu JS 05 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
27
courses/javascript/04_arrays_sets_und_maps/musterloesung.js
Normal file
27
courses/javascript/04_arrays_sets_und_maps/musterloesung.js
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
function verdoppeln(zahlen) {
|
||||||
|
return zahlen.map(x => x * 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterGeradeZahlen(zahlen) {
|
||||||
|
return zahlen.filter(x => x % 2 === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function arraySumme(zahlen) {
|
||||||
|
return zahlen.reduce((acc, curr) => acc + curr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function entferneDuplikate(liste) {
|
||||||
|
return [...new Set(liste)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function erstelleWoerterbuch(paare) {
|
||||||
|
return new Map(paare);
|
||||||
|
}
|
||||||
|
|
||||||
|
function zaehleHaeufigkeiten(woerter) {
|
||||||
|
const haeufigkeit = new Map();
|
||||||
|
for (const wort of woerter) {
|
||||||
|
haeufigkeit.set(wort, (haeufigkeit.get(wort) || 0) + 1);
|
||||||
|
}
|
||||||
|
return haeufigkeit;
|
||||||
|
}
|
||||||
44
courses/javascript/04_arrays_sets_und_maps/test_aufgabe.js
Normal file
44
courses/javascript/04_arrays_sets_und_maps/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// 🧪 Testsuite: JS 04 - Arrays, Sets & Maps
|
||||||
|
|
||||||
|
// Test 1: verdoppeln
|
||||||
|
assert.deepStrictEqual(verdoppeln([1, 2, 3]), [2, 4, 6], "verdoppeln([1, 2, 3]) sollte [2, 4, 6] sein");
|
||||||
|
assert.deepStrictEqual(verdoppeln([]), [], "verdoppeln([]) sollte [] sein");
|
||||||
|
assert.deepStrictEqual(verdoppeln([-2, 0, 5]), [-4, 0, 10], "verdoppeln([-2, 0, 5]) sollte [-4, 0, 10] sein");
|
||||||
|
|
||||||
|
// Test 2: filterGeradeZahlen
|
||||||
|
assert.deepStrictEqual(filterGeradeZahlen([1, 2, 3, 4, 5, 6]), [2, 4, 6], "filterGeradeZahlen() filtert unvollständig");
|
||||||
|
assert.deepStrictEqual(filterGeradeZahlen([1, 3, 5]), [], "filterGeradeZahlen([1, 3, 5]) sollte [] sein");
|
||||||
|
assert.deepStrictEqual(filterGeradeZahlen([]), [], "filterGeradeZahlen([]) sollte [] sein");
|
||||||
|
|
||||||
|
// Test 3: arraySumme
|
||||||
|
assert.strictEqual(arraySumme([10, 20, 30]), 60, "arraySumme([10, 20, 30]) sollte 60 sein");
|
||||||
|
assert.strictEqual(arraySumme([]), 0, "arraySumme([]) sollte 0 sein");
|
||||||
|
assert.strictEqual(arraySumme([5]), 5, "arraySumme([5]) sollte 5 sein");
|
||||||
|
|
||||||
|
// Test 4: entferneDuplikate (Set)
|
||||||
|
assert.deepStrictEqual(entferneDuplikate([1, 2, 2, 3, 1, 4]), [1, 2, 3, 4], "entferneDuplikate([1, 2, 2, 3, 1, 4]) sollte [1, 2, 3, 4] sein");
|
||||||
|
assert.deepStrictEqual(entferneDuplikate(["a", "b", "a", "c", "b"]), ["a", "b", "c"], "entferneDuplikate mit Strings fehlerhaft");
|
||||||
|
assert.deepStrictEqual(entferneDuplikate([]), [], "entferneDuplikate([]) sollte [] sein");
|
||||||
|
|
||||||
|
// Test 5: erstelleWoerterbuch (Map)
|
||||||
|
const wb = erstelleWoerterbuch([["de", "Hallo"], ["en", "Hello"], ["es", "Hola"]]);
|
||||||
|
assert.ok(wb instanceof Map, "erstelleWoerterbuch() muss eine Instanz von Map zurückgeben");
|
||||||
|
assert.strictEqual(wb.size, 3, "Map sollte 3 Einträge enthalten");
|
||||||
|
assert.strictEqual(wb.get("de"), "Hallo", "Map.get('de') sollte 'Hallo' sein");
|
||||||
|
assert.strictEqual(wb.get("en"), "Hello", "Map.get('en') sollte 'Hello' sein");
|
||||||
|
assert.strictEqual(wb.get("es"), "Hola", "Map.get('es') sollte 'Hola' sein");
|
||||||
|
assert.strictEqual(wb.has("fr"), false, "Map.has('fr') sollte false sein");
|
||||||
|
|
||||||
|
// Test 6: zaehleHaeufigkeiten (Map)
|
||||||
|
const counts = zaehleHaeufigkeiten(["apfel", "banane", "apfel", "orange", "banane", "apfel"]);
|
||||||
|
assert.ok(counts instanceof Map, "zaehleHaeufigkeiten() muss eine Map zurückgeben");
|
||||||
|
assert.strictEqual(counts.size, 3, "Map sollte 3 unterschiedliche Früchte enthalten");
|
||||||
|
assert.strictEqual(counts.get("apfel"), 3, "Apfel sollte 3x gezählt werden");
|
||||||
|
assert.strictEqual(counts.get("banane"), 2, "Banane sollte 2x gezählt werden");
|
||||||
|
assert.strictEqual(counts.get("orange"), 1, "Orange sollte 1x gezählt werden");
|
||||||
|
|
||||||
|
const emptyCounts = zaehleHaeufigkeiten([]);
|
||||||
|
assert.ok(emptyCounts instanceof Map, "Leeres Array muss eine leere Map zurückgeben");
|
||||||
|
assert.strictEqual(emptyCounts.size, 0, "Leere Map sollte size 0 haben");
|
||||||
|
|
||||||
|
console.log("✅ Alle 21 JavaScript Tests in JS 04 erfolgreich bestanden!");
|
||||||
|
|
@ -159,9 +159,9 @@ console.log(restlicheDaten); // { name: "Lisa", alter: 28, stadt: "Hamburg", sta
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=03_objekte_und_destructuring" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=05_objekte_und_destructuring" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">← Zurück zu JS 02</a>
|
<a href="../04_arrays_sets_und_maps/index.html" class="btn btn-secondary">← Zurück zu JS 04</a>
|
||||||
<a href="../04_dom_und_events/index.html" class="btn btn-secondary">Weiter zu JS 04 →</a>
|
<a href="../09_dom_und_events/index.html" class="btn btn-secondary">Weiter zu JS 06 →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
153
courses/javascript/06_klassen_und_oop/README.md
Normal file
153
courses/javascript/06_klassen_und_oop/README.md
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# 🌐 JS 06: Klassen & Objektorientierung (OOP)
|
||||||
|
|
||||||
|
Willkommen zu **Modul 06** des JavaScript-Kurses! In diesem Modul lernst du, wie moderne objektorientierte Programmierung (OOP) in JavaScript mit ES6-Klassensyntax funktioniert.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Lernziele
|
||||||
|
|
||||||
|
Nach diesem Modul kannst du:
|
||||||
|
1. **ES6-Klassen** mit `class`, `constructor` und Instanzmethoden definieren.
|
||||||
|
2. Den **`this`**-Kontext innerhalb von Klasseninstanzen verstehen und sicher anwenden.
|
||||||
|
3. Daten mit **Gettern (`get`) und Settern (`set`)** kapseln und vor fehlerhaften Werten schützen.
|
||||||
|
4. **Vererbung (`extends`)** nutzen und den Elternkonstruktor mit **`super(...)`** aufrufen.
|
||||||
|
5. **Statische Methoden & Fabrikfunktionen (`static`)** definieren und anwenden.
|
||||||
|
6. Objekt-Typen zur Laufzeit mit **`instanceof`** überprüfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Theoretische Grundlagen
|
||||||
|
|
||||||
|
### 1. Der Klassen-Bauplan (`class`, `constructor`, `this`)
|
||||||
|
|
||||||
|
Vor ECMAScript 2015 (ES6) basierte OOP in JavaScript rein auf Prototypen-Funktionen (`function Person() { ... } Person.prototype.sayHi = ...`). Die ES6-Klassensyntax bietet dafür eine moderne, lesbare und aus anderen Sprachen gewohnte Syntax:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Person {
|
||||||
|
constructor(vorname, nachname) {
|
||||||
|
this.vorname = vorname;
|
||||||
|
this.nachname = nachname;
|
||||||
|
}
|
||||||
|
|
||||||
|
vollstaendigerName() {
|
||||||
|
return `${this.vorname} ${this.nachname}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = new Person("Max", "Mustermann");
|
||||||
|
console.log(user.vollstaendigerName()); // "Max Mustermann"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Kapselung & Getter/Setter
|
||||||
|
|
||||||
|
Kapselung (Encapsulation) ist eines der 4 Grundprinzipien der OOP. Interne Zustände sollten nicht ungeschützt direkt verändert werden.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Bankkonto {
|
||||||
|
constructor(inhaber, startSaldo = 0) {
|
||||||
|
this.inhaber = inhaber;
|
||||||
|
this._saldo = startSaldo; // Konvention: _ kennzeichnet private/interne Felder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter ermöglicht das Lesen wie ein Attribut: konto.saldo
|
||||||
|
get saldo() {
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
|
||||||
|
einzahlen(betrag) {
|
||||||
|
if (typeof betrag !== "number" || betrag <= 0) {
|
||||||
|
throw new Error("Einzahlungsbetrag muss positiv sein.");
|
||||||
|
}
|
||||||
|
this._saldo += betrag;
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Vererbung mit `extends` und `super`
|
||||||
|
|
||||||
|
Kindklassen erben alle Methoden und Eigenschaften ihrer Basisklasse:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr) {
|
||||||
|
this.marke = marke;
|
||||||
|
this.modell = modell;
|
||||||
|
this.baujahr = baujahr;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
return `${this.marke} ${this.modell} (${this.baujahr})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElektroAuto extends Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr, batterieKapazitaet) {
|
||||||
|
// ⚠️ WICHTIG: super() MUSS vor dem ersten Zugriff auf 'this' aufgerufen werden!
|
||||||
|
super(marke, modell, baujahr);
|
||||||
|
this.batterieKapazitaet = batterieKapazitaet;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Überschreiben (Method Overriding):
|
||||||
|
beschreibung() {
|
||||||
|
return `${super.beschreibung()} [Elektro: ${this.batterieKapazitaet} kWh]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
reichweiteBerechnen(verbrauchPro100km) {
|
||||||
|
return (this.batterieKapazitaet / verbrauchPro100km) * 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Statische Methoden (`static`)
|
||||||
|
|
||||||
|
Statische Methoden werden direkt an der Klasse aufgerufen, ohne eine Instanz mit `new` erstellen zu müssen. Sie eignen sich perfekt als Fabrikmethoden (Factory Pattern):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
class ElektroAuto extends Fahrzeug {
|
||||||
|
// ...
|
||||||
|
static erstelleStandardTesla() {
|
||||||
|
return new ElektroAuto("Tesla", "Model 3", 2024, 75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto = ElektroAuto.erstelleStandardTesla();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏭 Didaktische Analogie: Der Bauplan & die Fabrikstraße
|
||||||
|
|
||||||
|
- **Klasse (`class`)**: Der technische CAD-Bauplan in den Köpfen der Ingenieure.
|
||||||
|
- **Instanz (`new`)**: Das fertig montierte Auto, das die Fabrikhalle verlässt.
|
||||||
|
- **Konstruktor (`constructor`)**: Das Fließband, das Rohkarosserien mit Motor und Farbe ausstattet.
|
||||||
|
- **Methoden (`this.fahren()`)**: Die Pedale und Funktionen des Fahrzeugs.
|
||||||
|
- **Statische Methoden (`static`)**: Die Roboterwerkzeuge in der Werkshalle.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgabenübersicht in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1: Bankkonto**:
|
||||||
|
- `constructor(inhaber, startSaldo = 0)`
|
||||||
|
- `get saldo`
|
||||||
|
- `einzahlen(betrag)`
|
||||||
|
- `abheben(betrag)`
|
||||||
|
2. **TODO 2: Fahrzeug**:
|
||||||
|
- `constructor(marke, modell, baujahr)`
|
||||||
|
- `beschreibung()`
|
||||||
|
3. **TODO 3: ElektroAuto extends Fahrzeug**:
|
||||||
|
- `constructor(marke, modell, baujahr, batterieKapazitaet)`
|
||||||
|
- `reichweiteBerechnen(verbrauchPro100km)`
|
||||||
|
- `beschreibung()`
|
||||||
|
4. **TODO 4: ElektroAuto.erstelleStandardTesla()**:
|
||||||
|
- Statische Fabrikmethode zur schnellen Erstellung eines Tesla Model 3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Führe die Testsuite in der Web-IDE oder lokal aus:
|
||||||
|
```bash
|
||||||
|
# In der Web-IDE: Klicke auf 'Code ausführen & testen'
|
||||||
|
```
|
||||||
98
courses/javascript/06_klassen_und_oop/aufgabe.js
Normal file
98
courses/javascript/06_klassen_und_oop/aufgabe.js
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 06: KLASSEN & OBJEKTORIENTIERUNG (OOP) 🌐
|
||||||
|
* ==================================================
|
||||||
|
* Moderne ES6-Klassen, Konstruktoren, Getter/Setter,
|
||||||
|
* Vererbung mit extends/super und statische Methoden.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): Klasse Bankkonto
|
||||||
|
// Erstelle eine Klasse Bankkonto mit Kapselung:
|
||||||
|
// - constructor(inhaber, startSaldo = 0):
|
||||||
|
// - Weist this.inhaber zu
|
||||||
|
// - Weist this._saldo das startSaldo zu (Default: 0)
|
||||||
|
// - Getter get saldo(): Gibt this._saldo zurück
|
||||||
|
// - Methode einzahlen(betrag):
|
||||||
|
// - Wirft einen Fehler (throw new Error("...")), wenn betrag <= 0
|
||||||
|
// - Erhöht this._saldo um betrag und gibt den neuen Saldo zurück
|
||||||
|
// - Methode abheben(betrag):
|
||||||
|
// - Wirft einen Fehler, wenn betrag <= 0
|
||||||
|
// - Wirft einen Fehler, wenn betrag > this._saldo ("Nicht genügend Guthaben")
|
||||||
|
// - Verringert this._saldo um betrag und gibt den neuen Saldo zurück
|
||||||
|
class Bankkonto {
|
||||||
|
constructor(inhaber, startSaldo = 0) {
|
||||||
|
// TODO: Initialisiere inhaber und _saldo
|
||||||
|
this.inhaber = inhaber;
|
||||||
|
this._saldo = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
get saldo() {
|
||||||
|
// TODO: Getter für _saldo implementieren
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
einzahlen(betrag) {
|
||||||
|
// TODO: Validierung und Erhöhung von _saldo
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
abheben(betrag) {
|
||||||
|
// TODO: Validierung, Guthabenprüfung und Reduzierung von _saldo
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): Basisklasse Fahrzeug
|
||||||
|
// Erstelle eine Basisklasse Fahrzeug:
|
||||||
|
// - constructor(marke, modell, baujahr):
|
||||||
|
// - Weist this.marke, this.modell und this.baujahr zu
|
||||||
|
// - Methode beschreibung():
|
||||||
|
// - Gibt einen String im Format "${marke} ${modell} (${baujahr})" zurück
|
||||||
|
class Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr) {
|
||||||
|
// TODO: Eigenschaften initialisieren
|
||||||
|
this.marke = marke;
|
||||||
|
this.modell = modell;
|
||||||
|
this.baujahr = baujahr;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
// TODO: String-Repräsentation zurückgeben
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): Kindklasse ElektroAuto extends Fahrzeug
|
||||||
|
// Erstelle eine Kindklasse ElektroAuto, die von Fahrzeug erbt:
|
||||||
|
// - constructor(marke, modell, baujahr, batterieKapazitaet):
|
||||||
|
// - Ruft super(marke, modell, baujahr) auf
|
||||||
|
// - Weist this.batterieKapazitaet (in kWh) zu
|
||||||
|
// - Methode reichweiteBerechnen(verbrauchPro100km):
|
||||||
|
// - Wirft einen Fehler, wenn verbrauchPro100km <= 0
|
||||||
|
// - Berechnet die Reichweite in km: (batterieKapazitaet / verbrauchPro100km) * 100
|
||||||
|
// - Überschreibe beschreibung():
|
||||||
|
// - Gibt "${super.beschreibung()} [Elektro: ${batterieKapazitaet} kWh]" zurück
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): Statische Fabrikmethode ElektroAuto.erstelleStandardTesla()
|
||||||
|
// - static erstelleStandardTesla():
|
||||||
|
// - Gibt eine neue Instanz von ElektroAuto mit ("Tesla", "Model 3", 2024, 75) zurück
|
||||||
|
class ElektroAuto extends Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr, batterieKapazitaet) {
|
||||||
|
// TODO: super(...) aufrufen und batterieKapazitaet initialisieren
|
||||||
|
super(marke, modell, baujahr);
|
||||||
|
this.batterieKapazitaet = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
// TODO: super.beschreibung() nutzen und um Batterie erweitern
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
reichweiteBerechnen(verbrauchPro100km) {
|
||||||
|
// TODO: Reichweite anhand von batterieKapazitaet und Verbrauch berechnen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static erstelleStandardTesla() {
|
||||||
|
// TODO: Neues ElektroAuto-Objekt mit Standard-Parametern erzeugen und zurückgeben
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
334
courses/javascript/06_klassen_und_oop/index.html
Normal file
334
courses/javascript/06_klassen_und_oop/index.html
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 06: Klassen & Objektorientierung (OOP)</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
<style>
|
||||||
|
.class-diagram {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 24px 0;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.uml-card {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
width: 280px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.uml-header {
|
||||||
|
background: #4f46e5;
|
||||||
|
color: #ffffff;
|
||||||
|
padding: 12px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
.uml-header.subclass {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
|
.uml-section {
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.uml-section:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.uml-title {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.uml-item {
|
||||||
|
padding: 3px 0;
|
||||||
|
color: var(--text-body);
|
||||||
|
}
|
||||||
|
.uml-arrow {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: #059669;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 Lehrpfad 2: Moderne Datenmodelle & OOP</div>
|
||||||
|
<h1>JS 06: Klassen & Objektorientierung (OOP)</h1>
|
||||||
|
<p>Lerne professionelle Datenmodellierung mit ES6-Klassen, Kapselung, Vererbung und statischen Fabrikmethoden.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: ES6 KLASSEN & KONSTRUKTOR -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🏗️ 1. Der Klassen-Bauplan: <code>class</code>, <code>constructor</code> & <code>this</code></h2>
|
||||||
|
<p>
|
||||||
|
Mit ECMAScript 2015 (ES6) führte JavaScript eine elegante, standardisierte Klassensyntax ein. Eine <strong>Klasse</strong> ist wie ein technischer Bauplan, aus dem mit dem Schlüsselwort <code>new</code> beliebig viele konkrete Objekte (Instanzen) gefertigt werden:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>class Benutzer {
|
||||||
|
// Der Konstruktor wird beim Erzeugen mit 'new' aufgerufen:
|
||||||
|
constructor(name, email) {
|
||||||
|
this.name = name; // Instanzvariable zuweisen
|
||||||
|
this.email = email;
|
||||||
|
this.erstelltAm = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methoden werden direkt im Klassenkörper definiert (ohne 'function'):
|
||||||
|
begruessen() {
|
||||||
|
return `Hallo, mein Name ist ${this.name}!`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instanziierung:
|
||||||
|
const user1 = new Benutzer("Anna Schmidt", "anna@dev.io");
|
||||||
|
console.log(user1.begruessen()); // "Hallo, mein Name ist Anna Schmidt!"</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: KAPSELUNG & GETTER / SETTER -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔒 2. Kapselung: Getters, Setters & Datenvalidierung</h2>
|
||||||
|
<p>
|
||||||
|
Objektorientierte Kapselung schützt interne Zustände vor unkontrollierten Änderungen von außen. Mit <code>get</code> und <code>set</code> steuerst du Lese- und Schreibzugriffe und validierst eingehende Werte:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>class Bankkonto {
|
||||||
|
constructor(inhaber, startSaldo = 0) {
|
||||||
|
this.inhaber = inhaber;
|
||||||
|
this._saldo = startSaldo; // Konvention: Unterstrich signalisiert internen Zustand
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getter: Zugriff wie auf eine normale Eigenschaft (konto.saldo)
|
||||||
|
get saldo() {
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kontrollierte Methoden statt direktem Schreibzugriff:
|
||||||
|
einzahlen(betrag) {
|
||||||
|
if (betrag <= 0) {
|
||||||
|
throw new Error("Einzahlungsbetrag muss positiv sein.");
|
||||||
|
}
|
||||||
|
this._saldo += betrag;
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🏭 Didaktische Analogie: Der Bauplan & die Fabrikstraße</div>
|
||||||
|
<p>
|
||||||
|
Stelle dir eine <strong>Klasse</strong> wie den CAD-Konstruktionsplan eines Autos in einer Autofabrik vor.
|
||||||
|
Jedes Mal, wenn du <code>new Auto(...)</code> ausführst, rollt ein eigenständiges Fahrzeug vom Fließband auf die Straße (die <strong>Instanz</strong>).
|
||||||
|
Das Auto besitzt eigene Farben und Seriennummern (<strong>Instanzvariablen</strong>), bedient sich über Pedale und Lenkrad (<strong>Methoden</strong>), und die Maschinen in der Fabrikhalle selbst sind die <strong>statischen Werkzeuge</strong>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: VERERBUNG & SUPER -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🧬 3. Vererbung mit <code>extends</code> & Elternaufruf <code>super()</code></h2>
|
||||||
|
<p>
|
||||||
|
Klassen können Eigenschaften und Methoden von Basisklassen erben. Mit <code>extends</code> leitet man eine Kindklasse ab und mit <code>super(...)</code> ruft man den Konstruktor oder die Methoden der Elternklasse auf:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>// Basisklasse:
|
||||||
|
class Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr) {
|
||||||
|
this.marke = marke;
|
||||||
|
this.modell = modell;
|
||||||
|
this.baujahr = baujahr;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
return `${this.marke} ${this.modell} (${this.baujahr})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Kindklasse erbt von Fahrzeug:
|
||||||
|
class ElektroAuto extends Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr, batterieKapazitaet) {
|
||||||
|
super(marke, modell, baujahr); // 1. Pflicht: Eltern-Konstruktor aufrufen!
|
||||||
|
this.batterieKapazitaet = batterieKapazitaet; // 2. Eigene Eigenschaften setzen
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methode der Elternklasse überschreiben & erweitern:
|
||||||
|
beschreibung() {
|
||||||
|
return `${super.beschreibung()} [Elektro: ${this.batterieKapazitaet} kWh]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
reichweiteBerechnen(verbrauchPro100km) {
|
||||||
|
return (this.batterieKapazitaet / verbrauchPro100km) * 100;
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<!-- KLASSENDIAGRAMM -->
|
||||||
|
<h3 style="margin-top: 24px;">📊 Klassenhierarchie (UML-Diagramm)</h3>
|
||||||
|
<div class="class-diagram">
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header">Fahrzeug (Basis)</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
<div class="uml-title">Attribute</div>
|
||||||
|
<div class="uml-item">+ marke: string</div>
|
||||||
|
<div class="uml-item">+ modell: string</div>
|
||||||
|
<div class="uml-item">+ baujahr: number</div>
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
<div class="uml-title">Methoden</div>
|
||||||
|
<div class="uml-item">+ beschreibung(): string</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="uml-arrow">← extends</div>
|
||||||
|
|
||||||
|
<div class="uml-card">
|
||||||
|
<div class="uml-header subclass">ElektroAuto (Kind)</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
<div class="uml-title">Zusatz-Attribute</div>
|
||||||
|
<div class="uml-item">+ batterieKapazitaet: number</div>
|
||||||
|
</div>
|
||||||
|
<div class="uml-section">
|
||||||
|
<div class="uml-title">Zusatz-Methoden</div>
|
||||||
|
<div class="uml-item">+ beschreibung(): string (override)</div>
|
||||||
|
<div class="uml-item">+ reichweiteBerechnen(v): number</div>
|
||||||
|
<div class="uml-item"><em>+ static erstelleStandardTesla()</em></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. THEORIE: STATISCHE METHODEN -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>⚙️ 4. Statische Methoden & Fabrikfunktionen (<code>static</code>)</h2>
|
||||||
|
<p>
|
||||||
|
Statische Methoden gehören zur Klasse selbst und nicht zu einer einzelnen Instanz. Man ruft sie direkt über <code>KlassenName.methode()</code> auf. Sie eignen sich hervorragend für Hilfsfunktionen und <strong>Fabrikmuster (Factory Methods)</strong>:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>class ElektroAuto extends Fahrzeug {
|
||||||
|
// ... Konstruktor und Methoden ...
|
||||||
|
|
||||||
|
// Statische Fabrikmethode erzeugt vorkonfigurierte Instanzen:
|
||||||
|
static erstelleStandardTesla() {
|
||||||
|
return new ElektroAuto("Tesla", "Model 3", 2024, 75);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aufruf direkt an der Klasse, ohne vorheriges 'new':
|
||||||
|
const meinTesla = ElektroAuto.erstelleStandardTesla();
|
||||||
|
console.log(meinTesla.beschreibung());
|
||||||
|
// "Tesla Model 3 (2024) [Elektro: 75 kWh]"</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 5. Cheat-Sheet: JavaScript OOP & ES6 Klassen</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Konzept</th>
|
||||||
|
<th style="padding: 10px;">Syntax-Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Bedeutung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Klassendeklaration</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>class Auto { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Definiert einen neuen Objekttypen-Bauplan</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Konstruktor</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>constructor(farbe) { this.farbe = farbe; }</code></td>
|
||||||
|
<td style="padding: 10px;">Initialisiert neue Objekt-Instanzen</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Getter / Setter</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>get saldo() { return this._saldo; }</code></td>
|
||||||
|
<td style="padding: 10px;">Kontrollierter Lese- und Schreibzugriff</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Vererbung</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>class Kind extends Basis { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Übernimmt Attribute & Methoden der Elternklasse</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Super-Aufruf</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>super(arg1, arg2);</code></td>
|
||||||
|
<td style="padding: 10px;">Führt den Eltern-Konstruktor oder Eltern-Methoden aus</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><strong>Statische Methode</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>static erzeuge() { return new X(); }</code></td>
|
||||||
|
<td style="padding: 10px;">Klassenbezogene Hilfs- oder Fabrikmethode</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><strong>Typ-Prüfung</strong></td>
|
||||||
|
<td style="padding: 10px;"><code>obj instanceof Klasse</code></td>
|
||||||
|
<td style="padding: 10px;">Prüft, ob <code>obj</code> aus <code>Klasse</code> oder deren Vererbungslinie stammt</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 6. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 6. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1">
|
||||||
|
<strong>TODO 1:</strong> Klasse <code>Bankkonto</code> mit <code>constructor(inhaber, startSaldo)</code>, Getter <code>saldo</code> sowie Methoden <code>einzahlen(betrag)</code> und <code>abheben(betrag)</code> implementieren.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2">
|
||||||
|
<strong>TODO 2:</strong> Basisklasse <code>Fahrzeug(marke, modell, baujahr)</code> mit Methode <code>beschreibung()</code> schreiben.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3">
|
||||||
|
<strong>TODO 3:</strong> Kindklasse <code>ElektroAuto extends Fahrzeug</code> mit <code>super()</code>, <code>batterieKapazitaet</code> und <code>reichweiteBerechnen(verbrauchPro100km)</code> implementieren.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4">
|
||||||
|
<strong>TODO 4:</strong> Statische Fabrikmethode <code>ElektroAuto.erstelleStandardTesla()</code> erstellen.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 3 & 4</div>
|
||||||
|
<p>
|
||||||
|
Vergiss nicht, in der Kindklasse <code>ElektroAuto</code> als allererste Anweisung im Konstruktor <code>super(marke, modell, baujahr)</code> aufzurufen, bevor du auf <code>this.batterieKapazitaet</code> zugreifst!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_2_oop_und_daten&chapter=06_klassen_und_oop" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../05_objekte_und_destructuring/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">← Zurück zu JS 03</a>
|
||||||
|
<a href="../07_regex_und_textverarbeitung/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu JS 07 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
74
courses/javascript/06_klassen_und_oop/musterloesung.js
Normal file
74
courses/javascript/06_klassen_und_oop/musterloesung.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: JS 06 – KLASSEN & OBJEKTORIENTIERUNG (OOP) 🌟
|
||||||
|
* =============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
class Bankkonto {
|
||||||
|
constructor(inhaber, startSaldo = 0) {
|
||||||
|
if (!inhaber || typeof inhaber !== "string") {
|
||||||
|
throw new Error("Inhaber muss als gültiger Text angegeben werden.");
|
||||||
|
}
|
||||||
|
if (typeof startSaldo !== "number" || startSaldo < 0 || isNaN(startSaldo)) {
|
||||||
|
throw new Error("Start-Saldo muss eine nicht-negative Zahl sein.");
|
||||||
|
}
|
||||||
|
this.inhaber = inhaber;
|
||||||
|
this._saldo = startSaldo;
|
||||||
|
}
|
||||||
|
|
||||||
|
get saldo() {
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
|
||||||
|
einzahlen(betrag) {
|
||||||
|
if (typeof betrag !== "number" || betrag <= 0 || isNaN(betrag)) {
|
||||||
|
throw new Error("Einzahlungsbetrag muss größer als 0 sein.");
|
||||||
|
}
|
||||||
|
this._saldo += betrag;
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
|
||||||
|
abheben(betrag) {
|
||||||
|
if (typeof betrag !== "number" || betrag <= 0 || isNaN(betrag)) {
|
||||||
|
throw new Error("Abhebebetrag muss größer als 0 sein.");
|
||||||
|
}
|
||||||
|
if (betrag > this._saldo) {
|
||||||
|
throw new Error("Nicht genügend Guthaben vorhanden.");
|
||||||
|
}
|
||||||
|
this._saldo -= betrag;
|
||||||
|
return this._saldo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr) {
|
||||||
|
this.marke = marke;
|
||||||
|
this.modell = modell;
|
||||||
|
this.baujahr = baujahr;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
return `${this.marke} ${this.modell} (${this.baujahr})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ElektroAuto extends Fahrzeug {
|
||||||
|
constructor(marke, modell, baujahr, batterieKapazitaet) {
|
||||||
|
super(marke, modell, baujahr);
|
||||||
|
this.batterieKapazitaet = batterieKapazitaet;
|
||||||
|
}
|
||||||
|
|
||||||
|
beschreibung() {
|
||||||
|
return `${super.beschreibung()} [Elektro: ${this.batterieKapazitaet} kWh]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
reichweiteBerechnen(verbrauchPro100km) {
|
||||||
|
if (typeof verbrauchPro100km !== "number" || verbrauchPro100km <= 0 || isNaN(verbrauchPro100km)) {
|
||||||
|
throw new Error("Verbrauch pro 100 km muss größer als 0 sein.");
|
||||||
|
}
|
||||||
|
return (this.batterieKapazitaet / verbrauchPro100km) * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
static erstelleStandardTesla() {
|
||||||
|
return new ElektroAuto("Tesla", "Model 3", 2024, 75);
|
||||||
|
}
|
||||||
|
}
|
||||||
103
courses/javascript/06_klassen_und_oop/test_aufgabe.js
Normal file
103
courses/javascript/06_klassen_und_oop/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
// 🧪 JS 06: Testsuite für Klassen, OOP, Vererbung & Methoden
|
||||||
|
|
||||||
|
if (typeof assert === "undefined") {
|
||||||
|
var assert = require("assert");
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefeFehler(fn, beschreibung) {
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
assert.ok(threw, `${beschreibung} sollte einen Fehler werfen!`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. Tests für Bankkonto
|
||||||
|
// ==========================================
|
||||||
|
const konto1 = new Bankkonto("Alice Schmidt");
|
||||||
|
assert.strictEqual(konto1.inhaber, "Alice Schmidt", "Bankkonto sollte korrekten Inhaber besitzen");
|
||||||
|
assert.strictEqual(konto1.saldo, 0, "Standard-Startsaldo sollte 0 sein");
|
||||||
|
|
||||||
|
const konto2 = new Bankkonto("Bob Berger", 250);
|
||||||
|
assert.strictEqual(konto2.saldo, 250, "Bankkonto sollte initialen Saldo von 250 haben");
|
||||||
|
|
||||||
|
// Einzahlen
|
||||||
|
const nachEinzahlung = konto2.einzahlen(150);
|
||||||
|
assert.strictEqual(nachEinzahlung, 400, "einzahlen(150) sollte neuen Saldo 400 zurückgeben");
|
||||||
|
assert.strictEqual(konto2.saldo, 400, "konto.saldo sollte nach Einzahlung 400 betragen");
|
||||||
|
|
||||||
|
pruefeFehler(() => konto2.einzahlen(0), "einzahlen(0)");
|
||||||
|
pruefeFehler(() => konto2.einzahlen(-50), "einzahlen(-50)");
|
||||||
|
|
||||||
|
// Abheben
|
||||||
|
const nachAbhebung = konto2.abheben(100);
|
||||||
|
assert.strictEqual(nachAbhebung, 300, "abheben(100) sollte neuen Saldo 300 zurückgeben");
|
||||||
|
assert.strictEqual(konto2.saldo, 300, "konto.saldo sollte nach Abhebung 300 betragen");
|
||||||
|
|
||||||
|
pruefeFehler(() => konto2.abheben(0), "abheben(0)");
|
||||||
|
pruefeFehler(() => konto2.abheben(-20), "abheben(-20)");
|
||||||
|
pruefeFehler(() => konto2.abheben(999), "abheben(999) bei Saldo 300");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. Tests für Fahrzeug
|
||||||
|
// ==========================================
|
||||||
|
const fahrzeug = new Fahrzeug("Volkswagen", "Golf 8", 2021);
|
||||||
|
assert.strictEqual(fahrzeug.marke, "Volkswagen", "Fahrzeug sollte Marke 'Volkswagen' besitzen");
|
||||||
|
assert.strictEqual(fahrzeug.modell, "Golf 8", "Fahrzeug sollte Modell 'Golf 8' besitzen");
|
||||||
|
assert.strictEqual(fahrzeug.baujahr, 2021, "Fahrzeug sollte Baujahr 2021 besitzen");
|
||||||
|
assert.strictEqual(
|
||||||
|
fahrzeug.beschreibung(),
|
||||||
|
"Volkswagen Golf 8 (2021)",
|
||||||
|
"Fahrzeug.beschreibung() sollte 'Marke Modell (Baujahr)' zurückgeben"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Tests für ElektroAuto (Vererbung & Methoden)
|
||||||
|
// ==========================================
|
||||||
|
const eAuto = new ElektroAuto("Hyundai", "Ioniq 5", 2023, 77.4);
|
||||||
|
|
||||||
|
assert.ok(eAuto instanceof ElektroAuto, "eAuto sollte Instanz von ElektroAuto sein");
|
||||||
|
assert.ok(eAuto instanceof Fahrzeug, "eAuto sollte via Vererbung Instanz von Fahrzeug sein");
|
||||||
|
|
||||||
|
assert.strictEqual(eAuto.marke, "Hyundai", "ElektroAuto erbt marke");
|
||||||
|
assert.strictEqual(eAuto.modell, "Ioniq 5", "ElektroAuto erbt modell");
|
||||||
|
assert.strictEqual(eAuto.baujahr, 2023, "ElektroAuto erbt baujahr");
|
||||||
|
assert.strictEqual(eAuto.batterieKapazitaet, 77.4, "ElektroAuto besitzt batterieKapazitaet");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
eAuto.beschreibung(),
|
||||||
|
"Hyundai Ioniq 5 (2023) [Elektro: 77.4 kWh]",
|
||||||
|
"ElektroAuto.beschreibung() sollte erweiterte Beschreibung liefern"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reichweitenberechnung: (77.4 kWh / 15.48 kWh/100km) * 100 = 500 km
|
||||||
|
const reichweite = eAuto.reichweiteBerechnen(15.48);
|
||||||
|
assert.strictEqual(Math.round(reichweite), 500, "reichweiteBerechnen(15.48) sollte 500 km ergeben");
|
||||||
|
|
||||||
|
pruefeFehler(() => eAuto.reichweiteBerechnen(0), "reichweiteBerechnen(0)");
|
||||||
|
pruefeFehler(() => eAuto.reichweiteBerechnen(-10), "reichweiteBerechnen(-10)");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. Tests für Statische Fabrikmethode
|
||||||
|
// ==========================================
|
||||||
|
const standardTesla = ElektroAuto.erstelleStandardTesla();
|
||||||
|
assert.ok(standardTesla instanceof ElektroAuto, "erstelleStandardTesla() muss ein ElektroAuto zurückgeben");
|
||||||
|
assert.strictEqual(standardTesla.marke, "Tesla", "Tesla Marke sollte 'Tesla' sein");
|
||||||
|
assert.strictEqual(standardTesla.modell, "Model 3", "Tesla Modell sollte 'Model 3' sein");
|
||||||
|
assert.strictEqual(standardTesla.baujahr, 2024, "Tesla Baujahr sollte 2024 sein");
|
||||||
|
assert.strictEqual(standardTesla.batterieKapazitaet, 75, "Tesla Batteriekapazität sollte 75 sein");
|
||||||
|
assert.strictEqual(
|
||||||
|
standardTesla.beschreibung(),
|
||||||
|
"Tesla Model 3 (2024) [Elektro: 75 kWh]",
|
||||||
|
"Tesla beschreibung() prüfen"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
standardTesla.reichweiteBerechnen(15),
|
||||||
|
500,
|
||||||
|
"Tesla Reichweite bei 15 kWh/100km sollte (75/15)*100 = 500 km sein"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("✅ Alle Tests in JS 06 (Klassen & OOP) erfolgreich bestanden!");
|
||||||
98
courses/javascript/07_regex_und_textverarbeitung/README.md
Normal file
98
courses/javascript/07_regex_und_textverarbeitung/README.md
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
# 🌐 JS 07: Reguläre Ausdrücke & Textverarbeitung
|
||||||
|
|
||||||
|
Willkommen zu **Modul 07** des JavaScript-Kurses! In diesem Modul lernst du die Kunst des **Pattern Matchings** und der **Textanalyse** mit Regulären Ausdrücken (RegExp).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Lernziele
|
||||||
|
|
||||||
|
Nach diesem Modul kannst du:
|
||||||
|
1. **Reguläre Ausdrücke (RegExp)** in JavaScript sowohl als Literal (`/muster/flags`) als auch über den Konstruktor (`new RegExp(...)`) erstellen.
|
||||||
|
2. **Flags** wie `g` (global), `i` (case-insensitive) und `m` (multiline) gezielt steuern.
|
||||||
|
3. **Metazeichen** (`\d`, `\w`, `\s`, `.`, `^`, `$`) und **Quantifizierer** (`+`, `*`, `?`, `{min,max}`) souverän kombinieren.
|
||||||
|
4. Strings mit **`regex.test()`** validieren und mit **`str.match()`** Fundstellen extrahieren.
|
||||||
|
5. Texte mit **`str.replace()`** und **Capturing Groups (`$1`, `$2`)** formatieren und sensible Daten maskieren.
|
||||||
|
6. Komplexe Validierungsregeln wie **Passwort- und E-Mail-Prüfungen** sauber umsetzen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Theoretische Grundlagen
|
||||||
|
|
||||||
|
### 1. Die zwei Wege zur RegExp-Erstellung
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Literal (schnell, statisch kompiliert, Standardfall):
|
||||||
|
const regexLiteral = /^[A-Z][a-z]+$/;
|
||||||
|
|
||||||
|
// 2. RegExp-Objekt (dynamisch, erlaubt Variablen im Suchmuster):
|
||||||
|
const userKeyword = "admin";
|
||||||
|
const regexDynamic = new RegExp(`^${userKeyword}$`, "i");
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Metazeichen-Übersicht
|
||||||
|
|
||||||
|
| Metazeichen | Bedeutung | Beispiel-Treffer |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `\d` | Ziffer (0-9) | `"5"`, `"9"` |
|
||||||
|
| `\D` | Keine Ziffer | `"a"`, `"!"` |
|
||||||
|
| `\w` | Wortzeichen (`[a-zA-Z0-9_]`) | `"x"`, `"7"`, `"_"` |
|
||||||
|
| `\W` | Kein Wortzeichen | `" "`, `"@"`, `"-"` |
|
||||||
|
| `\s` | Whitespace (Leerzeichen, Tab, Umbruch) | `" "`, `"\t"`, `"\n"` |
|
||||||
|
| `.` | Beliebiges Zeichen (außer Umbruch) | `"a"`, `"8"`, `"%"` |
|
||||||
|
| `^` | Textanfang | `/^Start/` |
|
||||||
|
| `$` | Textende | `/Ende$/` |
|
||||||
|
|
||||||
|
### 3. Quantifizierer (Häufigkeiten)
|
||||||
|
|
||||||
|
| Quantifizierer | Bedeutung | Beispiel |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| `+` | Mindestens 1 Mal (1 bis unendlich) | `\d+` matcht `"1234"` |
|
||||||
|
| `*` | 0 bis unendlich (optional) | `a*` matcht `""` oder `"aaa"` |
|
||||||
|
| `?` | 0 oder 1 Mal (optional) | `https?` matcht `"http"` & `"https"` |
|
||||||
|
| `{n}` | Exakt `n` Mal | `\d{4}` matcht `"2024"` |
|
||||||
|
| `{min,max}` | Zwischen `min` und `max` Mal | `\d{2,4}` |
|
||||||
|
| `{min,}` | Mindestens `min` Mal | `.{8,}` (mind. 8 Zeichen) |
|
||||||
|
|
||||||
|
### 4. Capturing Groups & Maskierung mit `replace`
|
||||||
|
|
||||||
|
Runde Klammern `( ... )` bilden Gruppen. Im Ersetzungs-String kann mit `$1`, `$2` usw. darauf zugegriffen werden:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Telefonnummer formatieren:
|
||||||
|
const nummer = "01711234567";
|
||||||
|
const formatiert = nummer.replace(/(\d{4})(\d+)/, "$1-$2");
|
||||||
|
console.log(formatiert); // "0171-1234567"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🕵️ Didaktische Analogie: Der Muster-Detektiv & Schablonen-Scanner
|
||||||
|
|
||||||
|
Ein Regulärer Ausdruck ist wie ein **Röntgengerät mit Lochmaske**:
|
||||||
|
- Du legst das Dokument (den Fließtext) auf den Scanner.
|
||||||
|
- Die Lochmaske blendet alle irrelevanten Wörter aus.
|
||||||
|
- Nur Abschnitte, die exakt in die Aussparungen passen (z. B. 4er-Zahlenblöcke für Kreditkarten oder das `@`-Zeichen mit Domain), leuchten grün auf.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgabenübersicht in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1: `istGueltigeEmail(email)`**:
|
||||||
|
- Prüft E-Mails auf das Format `name@domain.tld` (mind. 2-stellige TLD).
|
||||||
|
2. **TODO 2: `findeTelefonnummern(text)`**:
|
||||||
|
- Findet Telefonnummern wie `+49 170 1234567`, `0171-1234567` etc.
|
||||||
|
3. **TODO 3: `extrahiereHashtags(text)`**:
|
||||||
|
- Findet alle Social-Media-Tags (`#hashtag`).
|
||||||
|
4. **TODO 4: `maskiereKreditkarte(text)`**:
|
||||||
|
- Anonymisiert 16-stellige Kreditkartennummern zu `****-****-****-1234`.
|
||||||
|
5. **TODO 5: `istStarkesPasswort(passwort)`**:
|
||||||
|
- Prüft Passwortstärke (mind. 8 Zeichen, Klein-, Großbuchstaben, Ziffer, Sonderzeichen).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Führe die Testsuite in der Web-IDE oder lokal aus:
|
||||||
|
```bash
|
||||||
|
# In der Web-IDE: Klicke auf 'Code ausführen & testen'
|
||||||
|
```
|
||||||
61
courses/javascript/07_regex_und_textverarbeitung/aufgabe.js
Normal file
61
courses/javascript/07_regex_und_textverarbeitung/aufgabe.js
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 07: REGULÄRE AUSDRÜCKE & TEXTVERARBEITUNG 🌐
|
||||||
|
* ==================================================
|
||||||
|
* RegExp-Muster, Flags, Metazeichen, Quantifizierer
|
||||||
|
* und praktische Methoden (test, match, replace).
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): istGueltigeEmail(email)
|
||||||
|
// Prüfe, ob der gegebene String eine syntaktisch gültige E-Mail-Adresse ist.
|
||||||
|
// Kriterien:
|
||||||
|
// - Startet mit mind. 1 Zeichen (Buchstaben, Ziffern, ., _, %, +, -)
|
||||||
|
// - Enthält genau ein '@'
|
||||||
|
// - Domain-Teil mit mind. 1 Zeichen (Buchstaben, Ziffern, ., -)
|
||||||
|
// - Top-Level-Domain (TLD) mit mind. 2 Buchstaben am Ende (z. B. .de, .com, .info)
|
||||||
|
// Rückgabe: true wenn gültig, sonst false.
|
||||||
|
function istGueltigeEmail(email) {
|
||||||
|
// TODO: Regex-Muster definieren und mit .test() prüfen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): findeTelefonnummern(text)
|
||||||
|
// Finde alle Telefonnummern in einem beliebigen Fließtext.
|
||||||
|
// Unterstütze Formate wie "+49 170 1234567", "0171-1234567", "030 / 123456" oder "+49(0)30 123456".
|
||||||
|
// Rückgabe: Ein Array mit allen gefundenen Telefonnummern-Strings (oder [] wenn keine gefunden).
|
||||||
|
function findeTelefonnummern(text) {
|
||||||
|
// TODO: Regex mit Global-Flag 'g' erstellen und text.match(regex) nutzen
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): extrahiereHashtags(text)
|
||||||
|
// Extrahiere alle Social-Media-Hashtags aus einem Text (z. B. "#javascript", "#webdev2024").
|
||||||
|
// Ein Hashtag beginnt mit '#' gefolgt von einem oder mehreren Wortzeichen (Buchstaben, Zahlen, Unterstrich).
|
||||||
|
// Rückgabe: Array aller Hashtag-Strings inklusive Raute '#' (oder [] wenn keine vorhanden).
|
||||||
|
function extrahiereHashtags(text) {
|
||||||
|
// TODO: Hashtags mit Regex matchen und als Array zurückgeben
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): maskiereKreditkarte(text)
|
||||||
|
// Maskiere 16-stellige Kreditkartennummern aus Sicherheitsgründen im Text.
|
||||||
|
// Die Nummern können mit Bindestrichen ("1234-5678-9012-3456"), Leerzeichen ("1234 5678 9012 3456")
|
||||||
|
// oder durchgehend ("1234567890123456") geschrieben sein.
|
||||||
|
// Ersetze jede 16-stellige Kreditkarte durch das Format: "****-****-****-XXXX" (wobei XXXX die letzten 4 Ziffern sind).
|
||||||
|
// Rückgabe: Der bereinigte/maskierte Text-String.
|
||||||
|
function maskiereKreditkarte(text) {
|
||||||
|
// TODO: Mit regex.replace() und Capturing Group $1 für die letzten 4 Ziffern maskieren
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 5 (TODO 5): istStarkesPasswort(passwort)
|
||||||
|
// Validiere, ob ein Passwort den modernen Sicherheitsanforderungen entspricht:
|
||||||
|
// 1. Mindestens 8 Zeichen lang
|
||||||
|
// 2. Mindestens ein Kleinbuchstabe (a-z)
|
||||||
|
// 3. Mindestens ein Großbuchstabe (A-Z)
|
||||||
|
// 4. Mindestens eine Zahl (0-9)
|
||||||
|
// 5. Mindestens ein Sonderzeichen (z. B. ! @ # $ % ^ & * ( ) _ + - = [ ] { } ; ' : " \ | , . < > / ? ~)
|
||||||
|
// Rückgabe: true wenn stark, sonst false.
|
||||||
|
function istStarkesPasswort(passwort) {
|
||||||
|
// TODO: Bedingungen mit Regex prüfen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
239
courses/javascript/07_regex_und_textverarbeitung/index.html
Normal file
239
courses/javascript/07_regex_und_textverarbeitung/index.html
Normal file
|
|
@ -0,0 +1,239 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 07: Reguläre Ausdrücke & Textverarbeitung</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
<style>
|
||||||
|
.regex-card {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
.regex-token {
|
||||||
|
background: var(--bg-subtle);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 12px 14px;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
.token-char {
|
||||||
|
color: #7c3aed;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 1rem;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
.token-desc {
|
||||||
|
color: var(--text-body);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 Lehrpfad 2: Moderne Datenmodelle & OOP</div>
|
||||||
|
<h1>JS 07: Reguläre Ausdrücke & Textverarbeitung</h1>
|
||||||
|
<p>Lerne mächtiges Pattern-Matching, Text-Mining, Datenvalidierung und Maskierung mit JavaScript RegExp.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: WAS IST EIN REGEXP -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🔍 1. Grundlagen: RegExp-Syntax & Flags</h2>
|
||||||
|
<p>
|
||||||
|
Ein <strong>Regulärer Ausdruck (RegExp)</strong> ist ein Suchmuster zur Textanalyse, Validierung und Transformation. In JavaScript erstellst du Regex entweder als Literal mit Schrägstrichen <code>/muster/flags</code> oder über das <code>RegExp</code>-Objekt:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>// 1. Literal-Schreibweise (empfohlen bei festen Mustern):
|
||||||
|
const regexLiteral = /javascript/i; // 'i' = ignore case (Groß-/Kleinschreibung ignorieren)
|
||||||
|
|
||||||
|
// 2. Konstruktor-Schreibweise (für dynamische Muster aus Variablen):
|
||||||
|
const suchwort = "javascript";
|
||||||
|
const regexDynamic = new RegExp(suchwort, "i");
|
||||||
|
|
||||||
|
console.log(regexLiteral.test("Ich liebe JavaScript!")); // true</code></pre>
|
||||||
|
|
||||||
|
<h3 style="margin-top: 20px;">🚩 Wichtige Flags (Modifikatoren)</h3>
|
||||||
|
<ul>
|
||||||
|
<li><code>g</code> (global): Findet alle Vorkommen im gesamten Text (nicht nur das erste).</li>
|
||||||
|
<li><code>i</code> (case-insensitive): Ignoriert Unterschiede zwischen Groß- und Kleinschreibung.</li>
|
||||||
|
<li><code>m</code> (multiline): Behandelt Zeilenumbrüche für <code>^</code> (Zeilenanfang) und <code>$</code> (Zeilenende).</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: METAZEICHEN & QUANTIFIZIERER -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🧩 2. Metazeichen & Quantifizierer</h2>
|
||||||
|
<p>RegExp nutzt spezielle Steuerzeichen, um Zeichenklassen und Wiederholungen zu beschreiben:</p>
|
||||||
|
|
||||||
|
<div class="regex-card">
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">\d</div>
|
||||||
|
<div class="token-desc">Beliebige Ziffer (0-9)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">\w</div>
|
||||||
|
<div class="token-desc">Wortzeichen (a-z, A-Z, 0-9, _)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">\s</div>
|
||||||
|
<div class="token-desc">Leerzeichen, Tabulator, Zeilenumbruch</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">^ & $</div>
|
||||||
|
<div class="token-desc">Textanfang (<code>^</code>) & Textende (<code>$</code>)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">+</div>
|
||||||
|
<div class="token-desc">1 oder mehrmals (mind. 1)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">*</div>
|
||||||
|
<div class="token-desc">0 oder mehrmals (optional)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">?</div>
|
||||||
|
<div class="token-desc">0 oder 1 Mal (optional)</div>
|
||||||
|
</div>
|
||||||
|
<div class="regex-token">
|
||||||
|
<div class="token-char">{min,max}</div>
|
||||||
|
<div class="token-desc">Wiederholungsanzahl z. B. <code>{8,}</code> (mind. 8)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🕵️ Didaktische Analogie: Der Muster-Detektiv & Schablonen-Scanner</div>
|
||||||
|
<p>
|
||||||
|
Stelle dir einen <strong>Regulären Ausdruck</strong> wie eine transparente Loch-Schablone vor, die du über eine riesige Zeitungswand legst.
|
||||||
|
Die Löcher der Schablone sind so gestanzt, dass überall dort, wo eine E-Mail-Adresse, IBAN oder Telefonnummer steht, der Text sofort ins Auge springt, während der restliche Fließtext ausgeblendet wird.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: METHODEN -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>⚙️ 3. RegExp-Methoden in JavaScript</h2>
|
||||||
|
<p>Je nachdem, ob du prüfen, extrahieren oder ersetzen willst, bietet JS passende Methoden:</p>
|
||||||
|
|
||||||
|
<pre><code>const text = "Support: info@code.de oder sales@code.de. Tel: 0171-1234567.";
|
||||||
|
|
||||||
|
// 1. regex.test(str) -> boolean (perfekt für Validierung):
|
||||||
|
const hatEmail = /@code\.de/.test(text); // true
|
||||||
|
|
||||||
|
// 2. str.match(regex) -> Array mit allen Treffern (mit Flag 'g'):
|
||||||
|
const emails = text.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g);
|
||||||
|
console.log(emails); // ["info@code.de", "sales@code.de"]
|
||||||
|
|
||||||
|
// 3. str.replace(regex, neu) -> String ersetzen mit Capturing Groups:
|
||||||
|
const maskiert = text.replace(/(\d{4})-(\d+)/g, "$1-XXXX");
|
||||||
|
console.log(maskiert); // "... Tel: 0171-XXXX."</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: RegExp Syntax & Methoden</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Syntax / Methode</th>
|
||||||
|
<th style="padding: 10px;">Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Beschreibung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>/muster/flags</code></td>
|
||||||
|
<td style="padding: 10px;"><code>/\d+/g</code></td>
|
||||||
|
<td style="padding: 10px;">Regex Literal mit Flags</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>regex.test(str)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>/^[A-Z]/.test("Hallo") // true</code></td>
|
||||||
|
<td style="padding: 10px;">Prüft, ob Muster im String vorkommt (liefert <code>true</code>/<code>false</code>)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>str.match(regex)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>"12 a 34".match(/\d+/g) // ["12", "34"]</code></td>
|
||||||
|
<td style="padding: 10px;">Liefert Array aller Fundstellen</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>str.replace(regex, sub)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>"Hi".replace(/i/, "allo")</code></td>
|
||||||
|
<td style="padding: 10px;">Ersetzt Treffer durch Ersatz-String oder Funktion</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>[a-z0-9]</code></td>
|
||||||
|
<td style="padding: 10px;"><code>[A-Z] // Großbuchstabe</code></td>
|
||||||
|
<td style="padding: 10px;">Zeichenklasse (Matcht ein beliebiges Zeichen im Bereich)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>(gruppe) & $1</code></td>
|
||||||
|
<td style="padding: 10px;"><code>str.replace(/(\d+)/, "Nr: $1")</code></td>
|
||||||
|
<td style="padding: 10px;">Capturing Group zum Wiederverwenden von Teiltreffern</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 5. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1">
|
||||||
|
<strong>TODO 1:</strong> <code>istGueltigeEmail(email)</code> mit Regex-Validierung (<code>^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$</code>) schreiben.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2">
|
||||||
|
<strong>TODO 2:</strong> <code>findeTelefonnummern(text)</code> für verschiedene Formate (<code>+49...</code>, <code>0171-...</code>) implementieren.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3">
|
||||||
|
<strong>TODO 3:</strong> <code>extrahiereHashtags(text)</code> zur Extraktion aller <code>#hashtag</code> Wörter umsetzen.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4">
|
||||||
|
<strong>TODO 4:</strong> <code>maskiereKreditkarte(text)</code> zur Anonymisierung von 16-stelligen Kartennummern (<code>****-****-****-XXXX</code>) implementieren.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t5">
|
||||||
|
<label class="task-label" for="t5">
|
||||||
|
<strong>TODO 5:</strong> <code>istStarkesPasswort(passwort)</code> mit Check auf Länge (>=8), Klein-/Großbuchstaben, Ziffern und Sonderzeichen bauen.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 4</div>
|
||||||
|
<p>
|
||||||
|
In <code>str.replace(regex, replacement)</code> kannst du runde Klammern <code>(\d{4})</code> für die letzten 4 Ziffern verwenden und diese mit <code>$1</code> im Ersatz-String einfügen!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_2_oop_und_daten&chapter=07_regex_und_textverarbeitung" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../06_klassen_und_oop/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">← Zurück zu JS 06</a>
|
||||||
|
<a href="../08_fehlerbehandlung_und_debugging/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu JS 08 →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: JS 07 – REGEX & TEXTVERARBEITUNG 🌟
|
||||||
|
* ====================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function istGueltigeEmail(email) {
|
||||||
|
if (!email || typeof email !== "string") return false;
|
||||||
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
||||||
|
return emailRegex.test(email);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findeTelefonnummern(text) {
|
||||||
|
if (!text || typeof text !== "string") return [];
|
||||||
|
const telefonRegex = /(?:\+49|\+?\d{1,3}|0)[\d\s\/\-\(\)]{6,}\d/g;
|
||||||
|
const matches = text.match(telefonRegex);
|
||||||
|
return matches ? matches.map((m) => m.trim()) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extrahiereHashtags(text) {
|
||||||
|
if (!text || typeof text !== "string") return [];
|
||||||
|
const hashtagRegex = /#[a-zA-Z0-9_äöüÄÖÜß]+/g;
|
||||||
|
const matches = text.match(hashtagRegex);
|
||||||
|
return matches || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function maskiereKreditkarte(text) {
|
||||||
|
if (!text || typeof text !== "string") return "";
|
||||||
|
const ccRegex = /\b\d{4}[ -]?\d{4}[ -]?\d{4}[ -]?(\d{4})\b/g;
|
||||||
|
return text.replace(ccRegex, "****-****-****-$1");
|
||||||
|
}
|
||||||
|
|
||||||
|
function istStarkesPasswort(passwort) {
|
||||||
|
if (!passwort || typeof passwort !== "string" || passwort.length < 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const hatKlein = /[a-z]/.test(passwort);
|
||||||
|
const hatGross = /[A-Z]/.test(passwort);
|
||||||
|
const hatZahl = /\d/.test(passwort);
|
||||||
|
const hatSonderzeichen = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`§]/.test(passwort);
|
||||||
|
|
||||||
|
return hatKlein && hatGross && hatZahl && hatSonderzeichen;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// 🧪 JS 07: Testsuite für Reguläre Ausdrücke & Textverarbeitung
|
||||||
|
|
||||||
|
if (typeof assert === "undefined") {
|
||||||
|
var assert = require("assert");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. Tests für istGueltigeEmail
|
||||||
|
// ==========================================
|
||||||
|
assert.strictEqual(istGueltigeEmail("max@mustermann.de"), true, "max@mustermann.de sollte gültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail("user.name+tag@sub.domain.org"), true, "E-Mail mit Subdomain und Plus-Tag sollte gültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail("dev123@code.io"), true, "dev123@code.io sollte gültig sein");
|
||||||
|
|
||||||
|
assert.strictEqual(istGueltigeEmail("max@domain"), false, "E-Mail ohne TLD (.de, .com) sollte ungültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail("@domain.com"), false, "E-Mail ohne Benutzername sollte ungültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail("keine-email"), false, "Reiner Text sollte ungültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail(""), false, "Leerer String sollte ungültig sein");
|
||||||
|
assert.strictEqual(istGueltigeEmail(null), false, "null sollte ungültig sein");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. Tests für findeTelefonnummern
|
||||||
|
// ==========================================
|
||||||
|
const textMitNummern = "Kontakt: +49 170 1234567 oder per Festnetz 030 / 123456. Kein Datum 2024.";
|
||||||
|
const nummern = findeTelefonnummern(textMitNummern);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
nummern,
|
||||||
|
["+49 170 1234567", "030 / 123456"],
|
||||||
|
"Sollte Mobil- und Festnetznummern finden"
|
||||||
|
);
|
||||||
|
|
||||||
|
const mobilText = "Mobil: 0171-1234567 und 089 98765432.";
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
findeTelefonnummern(mobilText),
|
||||||
|
["0171-1234567", "089 98765432"],
|
||||||
|
"Sollte Bindestrich- und Leerzeichen-Format erkennen"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(findeTelefonnummern("Keine Telefonnummer hier im Jahr 2025!"), [], "Sollte leeres Array bei keinen Treffern liefern");
|
||||||
|
assert.deepStrictEqual(findeTelefonnummern(""), [], "Leerer Text sollte [] liefern");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Tests für extrahiereHashtags
|
||||||
|
// ==========================================
|
||||||
|
const post = "Lerne #javascript und #webdev mit modernen #es6_features!";
|
||||||
|
const tags = extrahiereHashtags(post);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
tags,
|
||||||
|
["#javascript", "#webdev", "#es6_features"],
|
||||||
|
"Sollte alle Hashtags im Text finden"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(extrahiereHashtags("Ein normaler Satz ohne Tags."), [], "Ohne Hashtags sollte [] zurückkommen");
|
||||||
|
assert.deepStrictEqual(extrahiereHashtags(""), [], "Leerer String sollte [] zurückgeben");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. Tests für maskiereKreditkarte
|
||||||
|
// ==========================================
|
||||||
|
const kkText = "Meine Karte ist 1234-5678-9012-3456.";
|
||||||
|
assert.strictEqual(
|
||||||
|
maskiereKreditkarte(kkText),
|
||||||
|
"Meine Karte ist ****-****-****-3456.",
|
||||||
|
"16-stellige Karte mit Bindestrichen sollte maskiert werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
const kkText2 = "Karten: 1111 2222 3333 4444 und 5555666677778888";
|
||||||
|
assert.strictEqual(
|
||||||
|
maskiereKreditkarte(kkText2),
|
||||||
|
"Karten: ****-****-****-4444 und ****-****-****-8888",
|
||||||
|
"Mehrere Karten mit Leerzeichen und durchgehend sollten maskiert werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
maskiereKreditkarte("Keine Kreditkarte vorhanden."),
|
||||||
|
"Keine Kreditkarte vorhanden.",
|
||||||
|
"Text ohne Kreditkarten darf nicht verändert werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 5. Tests für istStarkesPasswort
|
||||||
|
// ==========================================
|
||||||
|
assert.strictEqual(istStarkesPasswort("Sicher123!"), true, "Sicher123! sollte stark sein");
|
||||||
|
assert.strictEqual(istStarkesPasswort("P@ssw0rd2024"), true, "P@ssw0rd2024 sollte stark sein");
|
||||||
|
assert.strictEqual(istStarkesPasswort("Mein#Geheimnis9"), true, "Mein#Geheimnis9 sollte stark sein");
|
||||||
|
|
||||||
|
assert.strictEqual(istStarkesPasswort("S1!a"), false, "Zu kurzes Passwort (< 8 Zeichen) sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort("sicher123!"), false, "Ohne Großbuchstaben sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort("SICHER123!"), false, "Ohne Kleinbuchstaben sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort("SicheresPasswort!"), false, "Ohne Zahl sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort("Sicher12345"), false, "Ohne Sonderzeichen sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort(""), false, "Leerer String sollte false liefern");
|
||||||
|
assert.strictEqual(istStarkesPasswort(null), false, "null sollte false liefern");
|
||||||
|
|
||||||
|
console.log("✅ Alle Tests in JS 07 (Regex & Textverarbeitung) erfolgreich bestanden!");
|
||||||
106
courses/javascript/08_fehlerbehandlung_und_debugging/README.md
Normal file
106
courses/javascript/08_fehlerbehandlung_und_debugging/README.md
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
# 🌐 JS 08: Fehlerbehandlung & Debugging
|
||||||
|
|
||||||
|
Willkommen zu **Modul 08** des JavaScript-Kurses! In diesem Modul lernst du defensive Programmiertechniken, strukturierte Ausnahmebehandlung mit `try...catch...finally` und den Aufbau professioneller Fehlerklassen-Hierarchien.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Lernziele
|
||||||
|
|
||||||
|
Nach diesem Modul kannst du:
|
||||||
|
1. Das **`try...catch...finally`** Kontrollmuster verstehen und für fehlertolerante Anwendungen einsetzen.
|
||||||
|
2. Gezielte Fehler mit **`throw new Error(...)`** auslösen.
|
||||||
|
3. Eigene Fehlerklassen von **`Error`** ableiten (`class ValidierungsFehler extends Error`).
|
||||||
|
4. Fehlerklassen hierarchisch strukturieren und mit **Zusatzattributen** (wie betroffenes Formularfeld oder Saldo) ausstatten.
|
||||||
|
5. Im `catch`-Block unterschiedliche Fehlerursachen mittels **`instanceof`** unterscheiden und behandeln.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Theoretische Grundlagen
|
||||||
|
|
||||||
|
### 1. Das `try...catch...finally` Muster
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
// Kritischer Code:
|
||||||
|
const daten = JSON.parse(roheEingabe);
|
||||||
|
verarbeite(daten);
|
||||||
|
} catch (error) {
|
||||||
|
// Behandlung:
|
||||||
|
console.error("Fehler abgefangen:", error.message);
|
||||||
|
} finally {
|
||||||
|
// Läuft garantiert IMMER:
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Eigene Fehlerklassen definieren
|
||||||
|
|
||||||
|
Standard-Fehler (`new Error(...)`) enthalten oft nicht genügend Kontext für granulare UI-Reaktionen. Durch das Ableiten eigener Klassen schaffen wir Klarheit:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Basis für alle Fehler dieser Domäne:
|
||||||
|
class AnwendungsFehler extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AnwendungsFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spezifischer Validierungsfehler mit Feldbezug:
|
||||||
|
class ValidierungsFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, feld = null) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ValidierungsFehler";
|
||||||
|
this.feld = feld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Fehler differenzieren mit `instanceof`
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
try {
|
||||||
|
fuehreOperationAus();
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof ValidierungsFehler) {
|
||||||
|
console.warn(`Ungültige Eingabe im Feld ${err.feld}: ${err.message}`);
|
||||||
|
} else if (err instanceof GuthabenFehler) {
|
||||||
|
console.warn(`Guthaben (${err.saldo} €) reicht nicht für ${err.betrag} € aus.`);
|
||||||
|
} else {
|
||||||
|
// Unbekannter Fehler -> Neu werfen
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🪂 Didaktische Analogie: Die Notbremse & der Auffang-Fallschirm
|
||||||
|
|
||||||
|
- **`throw`**: Das Ziehen der Notbremse im Zug – der normale Ablauf wird sofort unterbrochen.
|
||||||
|
- **`catch`**: Der Fallschirm, der die Passagiere vor dem Absturz bewahrt und einen sicheren Ausstieg ermöglicht.
|
||||||
|
- **`finally`**: Der Flughafen-Boden, der am Ende des Flugs immer erreicht wird, egal wie turbulent die Reise war.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgabenübersicht in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1: Error-Klassenhierarchie**:
|
||||||
|
- `AnwendungsFehler extends Error`
|
||||||
|
- `ValidierungsFehler extends AnwendungsFehler` (mit `feld`)
|
||||||
|
- `GuthabenFehler extends AnwendungsFehler` (mit `saldo`, `betrag`)
|
||||||
|
2. **TODO 2: `sichereDivision(a, b)`**:
|
||||||
|
- Prüft Zahlen und fängt Division durch 0 mit `AnwendungsFehler` ab.
|
||||||
|
3. **TODO 3: `parseBenutzerJson(jsonString)`**:
|
||||||
|
- Fängt JSON-Syntaxfehler ab und validiert das Feld `name`.
|
||||||
|
4. **TODO 4: `pruefeTransaktion(kontoSaldo, abhebeBetrag)`**:
|
||||||
|
- Prüft positive Beträge und ausreichendes Guthaben mit `GuthabenFehler`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Führe die Testsuite in der Web-IDE oder lokal aus:
|
||||||
|
```bash
|
||||||
|
# In der Web-IDE: Klicke auf 'Code ausführen & testen'
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 08: FEHLERBEHANDLUNG & DEBUGGING 🌐
|
||||||
|
* ============================================
|
||||||
|
* Defensive Programmierung mit try/catch/finally,
|
||||||
|
* benutzerdefinierten Error-Hierarchien und instanceof.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 1 (TODO 1): Eigene Fehlerklassen ableiten
|
||||||
|
// Erstelle eine modulare Fehler-Hierarchie:
|
||||||
|
//
|
||||||
|
// 1. AnwendungsFehler extends Error:
|
||||||
|
// - constructor(message)
|
||||||
|
// - Setzt super(message) und this.name = "AnwendungsFehler"
|
||||||
|
//
|
||||||
|
// 2. ValidierungsFehler extends AnwendungsFehler:
|
||||||
|
// - constructor(message, feld = null)
|
||||||
|
// - Setzt super(message), this.name = "ValidierungsFehler" und this.feld = feld
|
||||||
|
//
|
||||||
|
// 3. GuthabenFehler extends AnwendungsFehler:
|
||||||
|
// - constructor(message, saldo = 0, betrag = 0)
|
||||||
|
// - Setzt super(message), this.name = "GuthabenFehler", this.saldo = saldo und this.betrag = betrag
|
||||||
|
|
||||||
|
class AnwendungsFehler extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
// TODO: super(message) aufrufen und this.name setzen
|
||||||
|
super(message);
|
||||||
|
this.name = "AnwendungsFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidierungsFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, feld = null) {
|
||||||
|
// TODO: super(message) aufrufen, this.name und this.feld setzen
|
||||||
|
super(message);
|
||||||
|
this.name = "ValidierungsFehler";
|
||||||
|
this.feld = feld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuthabenFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, saldo = 0, betrag = 0) {
|
||||||
|
// TODO: super(message) aufrufen, this.name, this.saldo und this.betrag setzen
|
||||||
|
super(message);
|
||||||
|
this.name = "GuthabenFehler";
|
||||||
|
this.saldo = saldo;
|
||||||
|
this.betrag = betrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 2 (TODO 2): sichereDivision(a, b)
|
||||||
|
// Führe eine sichere mathematische Division aus:
|
||||||
|
// - Wenn a oder b keine gültige Zahl ist (typeof !== 'number' oder isNaN):
|
||||||
|
// -> Wirft ValidierungsFehler("Ungültige Zahlenwerte für Division.", "parameter")
|
||||||
|
// - Wenn b === 0:
|
||||||
|
// -> Wirft AnwendungsFehler("Division durch Null ist nicht erlaubt.")
|
||||||
|
// - Rückgabe: Das Ergebnis von a / b
|
||||||
|
function sichereDivision(a, b) {
|
||||||
|
// TODO: Validieren, Fehler werfen oder Division berechnen
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 3 (TODO 3): parseBenutzerJson(jsonString)
|
||||||
|
// Parse einen JSON-String und validiere das Ergebnis mit try...catch:
|
||||||
|
// - Fange eventuelle JSON-SyntaxError mit try...catch ab und wirf stattdessen:
|
||||||
|
// -> ValidierungsFehler("Ungültiges JSON-Format: ...", "jsonString")
|
||||||
|
// - Validiere das geparste Objekt:
|
||||||
|
// - Wenn kein Objekt oder wenn die Eigenschaft 'name' (nicht-leerer String) fehlt:
|
||||||
|
// -> Wirf ValidierungsFehler("Pflichtfeld 'name' fehlt oder ist ungültig.", "name")
|
||||||
|
// - Rückgabe: Das validierte Benutzerobjekt
|
||||||
|
function parseBenutzerJson(jsonString) {
|
||||||
|
// TODO: JSON.parse in try...catch ausführen und Felder validieren
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 TEILZIEL 4 (TODO 4): pruefeTransaktion(kontoSaldo, abhebeBetrag)
|
||||||
|
// Prüfe und simuliere eine Kontotransaktion:
|
||||||
|
// - Wenn kontoSaldo oder abhebeBetrag keine gültigen Zahlen sind oder abhebeBetrag <= 0:
|
||||||
|
// -> Wirft ValidierungsFehler("Ungültiger Abhebebetrag.", "abhebeBetrag")
|
||||||
|
// - Wenn kontoSaldo < abhebeBetrag:
|
||||||
|
// -> Wirft GuthabenFehler("Nicht genügend Guthaben vorhanden.", kontoSaldo, abhebeBetrag)
|
||||||
|
// - Rückgabe: Der verbleibende Saldo (kontoSaldo - abhebeBetrag)
|
||||||
|
function pruefeTransaktion(kontoSaldo, abhebeBetrag) {
|
||||||
|
// TODO: Betrag & Guthaben prüfen und neuen Saldo zurückgeben
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
251
courses/javascript/08_fehlerbehandlung_und_debugging/index.html
Normal file
251
courses/javascript/08_fehlerbehandlung_und_debugging/index.html
Normal file
|
|
@ -0,0 +1,251 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 08: Fehlerbehandlung & Debugging</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
<style>
|
||||||
|
.flow-diagram {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 24px 0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.88rem;
|
||||||
|
}
|
||||||
|
.flow-step {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-weight: 700;
|
||||||
|
text-align: center;
|
||||||
|
min-width: 260px;
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
.flow-try {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #3730a3;
|
||||||
|
border: 2px solid #818cf8;
|
||||||
|
}
|
||||||
|
.flow-branch {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.flow-ok {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #166534;
|
||||||
|
border: 2px solid #4ade80;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.flow-catch {
|
||||||
|
background: #ffe4e6;
|
||||||
|
color: #9f1239;
|
||||||
|
border: 2px solid #fb7185;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.flow-finally {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #92400e;
|
||||||
|
border: 2px solid #fbbf24;
|
||||||
|
}
|
||||||
|
.flow-arrow {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🌐 Lehrpfad 2: Moderne Datenmodelle & OOP</div>
|
||||||
|
<h1>JS 08: Fehlerbehandlung & Debugging</h1>
|
||||||
|
<p>Schreibe robusten und defensiven JavaScript-Code mit try/catch/finally, benutzerdefinierten Fehler-Klassen und instanceof.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 1. THEORIE: TRY CATCH FINALLY -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🛑 1. Das <code>try...catch...finally</code> Kontrollmuster</h2>
|
||||||
|
<p>
|
||||||
|
Laufzeitfehler bringen ungesicherte Programme sofort zum Absturz. Mit <code>try...catch</code> fängst du Ausnahmen sicher ab und ermöglichst deiner Webanwendung eine kontrollierte Fehlerbehandlung:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>try {
|
||||||
|
// 1. Code ausführen, der fehlschlagen könnte:
|
||||||
|
const daten = JSON.parse(roheEingabe);
|
||||||
|
verarbeite(daten);
|
||||||
|
} catch (error) {
|
||||||
|
// 2. Wird nur ausgeführt, wenn im try-Block ein Fehler aufgetreten ist:
|
||||||
|
console.error("Fehler aufgetreten:", error.name, error.message);
|
||||||
|
} finally {
|
||||||
|
// 3. Wird IMMER ausgeführt (z. B. für Aufräumarbeiten, Lade-Spinner stoppen):
|
||||||
|
schliesseLadeAnzeige();
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<!-- FLOWCHART -->
|
||||||
|
<h3 style="margin-top: 20px;">📊 Kontrollfluss-Diagramm</h3>
|
||||||
|
<div class="flow-diagram">
|
||||||
|
<div class="flow-step flow-try">1. <code>try { ... }</code> (Code ausführen)</div>
|
||||||
|
<div class="flow-arrow">↓ Tritt Fehler auf?</div>
|
||||||
|
<div class="flow-branch">
|
||||||
|
<div class="flow-step flow-ok">✅ Nein: Normal weiter</div>
|
||||||
|
<div class="flow-step flow-catch">❌ Ja: <code>catch(err) { ... }</code></div>
|
||||||
|
</div>
|
||||||
|
<div class="flow-arrow">↓</div>
|
||||||
|
<div class="flow-step flow-finally">3. <code>finally { ... }</code> (Aufräumen – läuft immer!)</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: THROW & EIGENE ERROR KLASSEN -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>💣 2. Fehler werfen mit <code>throw</code> & Eigene Error-Klassen</h2>
|
||||||
|
<p>
|
||||||
|
Mit dem <code>throw</code>-Befehl löst du gezielt Fehler aus. Statt generischer Strings wirft man in professionellem JavaScript immer Instanzen von <code>Error</code> (oder abgeleiteten Klassen):
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>// Basisklasse für alle Domänenfehler der App:
|
||||||
|
class AnwendungsFehler extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AnwendungsFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spezifische Kindklasse mit Zusatzinformationen:
|
||||||
|
class ValidierungsFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, feld = null) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ValidierungsFehler";
|
||||||
|
this.feld = feld; // Zusätzliche Metadaten für das UI-Highlighting
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🪂 Didaktische Analogie: Die Notbremse & der Auffang-Fallschirm</div>
|
||||||
|
<p>
|
||||||
|
Einen Fehler mit <code>throw new ValidierungsFehler()</code> zu werfen, ist wie das Ziehen der <strong>Sicherheits-Notbremse</strong> im fahrenden Zug: Der normale Ablauf stoppt sofort.
|
||||||
|
Der <code>catch</code>-Block ist der <strong>Auffang-Fallschirm</strong>, der den Sturz abfedert und dafür sorgt, dass die Passagiere (deine Web-App-Benutzer) sicher landen, statt ins Nichts abzustürzen.
|
||||||
|
Der <code>finally</code>-Block ist die <strong>Landestation</strong>, die in jedem Fall erreicht wird.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: INSTANCEOF & DIFFERENZIERUNG -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 3. Fehlerarten gezielt unterscheiden mit <code>instanceof</code></h2>
|
||||||
|
<p>
|
||||||
|
Im <code>catch</code>-Block können unterschiedliche Fehlerarten auftreten. Mit dem <code>instanceof</code>-Operator kannst du gezielt auf Validierungsfehler anders reagieren als auf System- oder Netzwerkfehler:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<pre><code>try {
|
||||||
|
sendeZahlung(konto, 500);
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof GuthabenFehler) {
|
||||||
|
zeigeNutzerHinweis(`Nicht genügend Guthaben! Es fehlen ${error.betrag - error.saldo} €.`);
|
||||||
|
} else if (error instanceof ValidierungsFehler) {
|
||||||
|
markiereFormularFeldRot(error.feld, error.message);
|
||||||
|
} else {
|
||||||
|
// Unbekannter Systemfehler -> Erneut werfen oder an Monitoring-Service melden:
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: Error Handling & Debugging</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Befehl / Syntax</th>
|
||||||
|
<th style="padding: 10px;">Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Wirkung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>throw new Error(...)</code></td>
|
||||||
|
<td style="padding: 10px;"><code>throw new Error("Ungültig!");</code></td>
|
||||||
|
<td style="padding: 10px;">Löst Ausnahme aus und bricht aktuellen Stack ab</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>try { } catch(e) { }</code></td>
|
||||||
|
<td style="padding: 10px;"><code>try { ... } catch (e) { ... }</code></td>
|
||||||
|
<td style="padding: 10px;">Fängt Laufzeitfehler ab und verhindert Crash</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>finally { }</code></td>
|
||||||
|
<td style="padding: 10px;"><code>finally { cleanup(); }</code></td>
|
||||||
|
<td style="padding: 10px;">Wird garantiert ausgeführt (mit oder ohne Fehler)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>class X extends Error</code></td>
|
||||||
|
<td style="padding: 10px;"><code>class MeinFehler extends Error { }</code></td>
|
||||||
|
<td style="padding: 10px;">Eigene typisierte Fehlerklasse mit Stacktrace</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>err instanceof X</code></td>
|
||||||
|
<td style="padding: 10px;"><code>if (e instanceof ValidierungsFehler)</code></td>
|
||||||
|
<td style="padding: 10px;">Prüft die Fehlerklasse zur differenzierten Behandlung</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 5. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1">
|
||||||
|
<strong>TODO 1:</strong> Fehlerhierarchie <code>AnwendungsFehler extends Error</code>, <code>ValidierungsFehler extends AnwendungsFehler</code> und <code>GuthabenFehler extends AnwendungsFehler</code> erstellen.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2">
|
||||||
|
<strong>TODO 2:</strong> <code>sichereDivision(a, b)</code> mit Fehlerprüfungen für Nicht-Zahlen (<code>ValidierungsFehler</code>) und Division durch Null (<code>AnwendungsFehler</code>) schreiben.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3">
|
||||||
|
<strong>TODO 3:</strong> <code>parseBenutzerJson(jsonString)</code> mit <code>try...catch</code> für Syntaxfehler und Pflichtfeldprüfung (<code>name</code>) umsetzen.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4">
|
||||||
|
<strong>TODO 4:</strong> <code>pruefeTransaktion(kontoSaldo, abhebeBetrag)</code> mit <code>ValidierungsFehler</code> und <code>GuthabenFehler</code> absichern.
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-tipp" style="margin-top: 20px;">
|
||||||
|
<div class="box-title">💡 Tipp für Teilziel 3</div>
|
||||||
|
<p>
|
||||||
|
In <code>parseBenutzerJson</code> kannst du <code>JSON.parse(jsonString)</code> in einem inneren <code>try...catch</code>-Block aufrufen. Fange den <code>SyntaxError</code> ab und wirf stattdessen einen <code>new ValidierungsFehler(..., "jsonString")</code>!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_2_oop_und_daten&chapter=08_fehlerbehandlung_und_debugging" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../07_regex_und_textverarbeitung/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">← Zurück zu JS 07</a>
|
||||||
|
<a href="../11_callbacks_und_promises/index.html" class="btn btn-secondary" style="text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu Lehrpfad 3 (Async) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: JS 08 – FEHLERBEHANDLUNG & DEBUGGING 🌟
|
||||||
|
* =======================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AnwendungsFehler extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message);
|
||||||
|
this.name = "AnwendungsFehler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ValidierungsFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, feld = null) {
|
||||||
|
super(message);
|
||||||
|
this.name = "ValidierungsFehler";
|
||||||
|
this.feld = feld;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GuthabenFehler extends AnwendungsFehler {
|
||||||
|
constructor(message, saldo = 0, betrag = 0) {
|
||||||
|
super(message);
|
||||||
|
this.name = "GuthabenFehler";
|
||||||
|
this.saldo = saldo;
|
||||||
|
this.betrag = betrag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sichereDivision(a, b) {
|
||||||
|
if (typeof a !== "number" || typeof b !== "number" || isNaN(a) || isNaN(b)) {
|
||||||
|
throw new ValidierungsFehler("Ungültige Zahlenwerte für Division.", "parameter");
|
||||||
|
}
|
||||||
|
if (b === 0) {
|
||||||
|
throw new AnwendungsFehler("Division durch Null ist nicht erlaubt.");
|
||||||
|
}
|
||||||
|
return a / b;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBenutzerJson(jsonString) {
|
||||||
|
if (!jsonString || typeof jsonString !== "string") {
|
||||||
|
throw new ValidierungsFehler("JSON-String darf nicht leer sein.", "jsonString");
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(jsonString);
|
||||||
|
} catch (err) {
|
||||||
|
throw new ValidierungsFehler("Ungültiges JSON-Format: " + err.message, "jsonString");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
throw new ValidierungsFehler("JSON muss ein Objekt darstellen.", "root");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.name || typeof parsed.name !== "string" || parsed.name.trim() === "") {
|
||||||
|
throw new ValidierungsFehler("Pflichtfeld 'name' fehlt oder ist ungültig.", "name");
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruefeTransaktion(kontoSaldo, abhebeBetrag) {
|
||||||
|
if (
|
||||||
|
typeof kontoSaldo !== "number" ||
|
||||||
|
typeof abhebeBetrag !== "number" ||
|
||||||
|
isNaN(kontoSaldo) ||
|
||||||
|
isNaN(abhebeBetrag) ||
|
||||||
|
abhebeBetrag <= 0
|
||||||
|
) {
|
||||||
|
throw new ValidierungsFehler("Ungültiger Abhebebetrag.", "abhebeBetrag");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kontoSaldo < abhebeBetrag) {
|
||||||
|
throw new GuthabenFehler("Nicht genügend Guthaben vorhanden.", kontoSaldo, abhebeBetrag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return kontoSaldo - abhebeBetrag;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,115 @@
|
||||||
|
// 🧪 JS 08: Testsuite für Fehlerbehandlung, Debugging & Error-Klassen
|
||||||
|
|
||||||
|
if (typeof assert === "undefined") {
|
||||||
|
var assert = require("assert");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 1. Tests für die Error-Klassenhierarchie
|
||||||
|
// ==========================================
|
||||||
|
const appErr = new AnwendungsFehler("Basis Anwendungsfehler");
|
||||||
|
assert.ok(appErr instanceof Error, "AnwendungsFehler muss von Error erben");
|
||||||
|
assert.ok(appErr instanceof AnwendungsFehler, "appErr muss Instanz von AnwendungsFehler sein");
|
||||||
|
assert.strictEqual(appErr.name, "AnwendungsFehler", "appErr.name sollte 'AnwendungsFehler' sein");
|
||||||
|
assert.strictEqual(appErr.message, "Basis Anwendungsfehler", "appErr.message prüfen");
|
||||||
|
|
||||||
|
const valErr = new ValidierungsFehler("Ungültige E-Mail", "email");
|
||||||
|
assert.ok(valErr instanceof Error, "ValidierungsFehler erbt von Error");
|
||||||
|
assert.ok(valErr instanceof AnwendungsFehler, "ValidierungsFehler erbt von AnwendungsFehler");
|
||||||
|
assert.ok(valErr instanceof ValidierungsFehler, "valErr ist Instanz von ValidierungsFehler");
|
||||||
|
assert.strictEqual(valErr.name, "ValidierungsFehler", "valErr.name sollte 'ValidierungsFehler' sein");
|
||||||
|
assert.strictEqual(valErr.feld, "email", "valErr.feld sollte 'email' sein");
|
||||||
|
|
||||||
|
const gutErr = new GuthabenFehler("Zu wenig Geld", 50, 100);
|
||||||
|
assert.ok(gutErr instanceof Error, "GuthabenFehler erbt von Error");
|
||||||
|
assert.ok(gutErr instanceof AnwendungsFehler, "GuthabenFehler erbt von AnwendungsFehler");
|
||||||
|
assert.ok(gutErr instanceof GuthabenFehler, "gutErr ist Instanz von GuthabenFehler");
|
||||||
|
assert.strictEqual(gutErr.name, "GuthabenFehler", "gutErr.name sollte 'GuthabenFehler' sein");
|
||||||
|
assert.strictEqual(gutErr.saldo, 50, "gutErr.saldo sollte 50 sein");
|
||||||
|
assert.strictEqual(gutErr.betrag, 100, "gutErr.betrag sollte 100 sein");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 2. Tests für sichereDivision
|
||||||
|
// ==========================================
|
||||||
|
assert.strictEqual(sichereDivision(10, 2), 5, "sichereDivision(10, 2) sollte 5 ergeben");
|
||||||
|
assert.strictEqual(sichereDivision(-20, 4), -5, "sichereDivision(-20, 4) sollte -5 ergeben");
|
||||||
|
|
||||||
|
// Division durch Null
|
||||||
|
let divNullGefangen = false;
|
||||||
|
try {
|
||||||
|
sichereDivision(10, 0);
|
||||||
|
} catch (err) {
|
||||||
|
divNullGefangen = true;
|
||||||
|
assert.ok(err instanceof AnwendungsFehler, "Division durch Null sollte AnwendungsFehler werfen");
|
||||||
|
}
|
||||||
|
assert.ok(divNullGefangen, "Division durch 0 muss einen Fehler werfen");
|
||||||
|
|
||||||
|
// Ungültige Typen
|
||||||
|
let invalidTypeGefangen = false;
|
||||||
|
try {
|
||||||
|
sichereDivision("abc", 2);
|
||||||
|
} catch (err) {
|
||||||
|
invalidTypeGefangen = true;
|
||||||
|
assert.ok(err instanceof ValidierungsFehler, "Nicht-Zahlen sollten ValidierungsFehler werfen");
|
||||||
|
}
|
||||||
|
assert.ok(invalidTypeGefangen, "sichereDivision mit Strings muss ValidierungsFehler werfen");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 3. Tests für parseBenutzerJson
|
||||||
|
// ==========================================
|
||||||
|
const validJson = '{"name": "Lisa", "alter": 28}';
|
||||||
|
const userObj = parseBenutzerJson(validJson);
|
||||||
|
assert.deepStrictEqual(userObj, { name: "Lisa", alter: 28 }, "Gültiges JSON sollte geparst zurückgegeben werden");
|
||||||
|
|
||||||
|
// Syntaxfehler im JSON
|
||||||
|
let syntaxErrGefangen = false;
|
||||||
|
try {
|
||||||
|
parseBenutzerJson('{ ungueltiges: json ]');
|
||||||
|
} catch (err) {
|
||||||
|
syntaxErrGefangen = true;
|
||||||
|
assert.ok(err instanceof ValidierungsFehler, "Syntaxfehler muss als ValidierungsFehler abgefangen werden");
|
||||||
|
assert.strictEqual(err.feld, "jsonString", "Fehlerfeld bei SyntaxError sollte 'jsonString' sein");
|
||||||
|
}
|
||||||
|
assert.ok(syntaxErrGefangen, "Ungültiges JSON muss abgefangen werden");
|
||||||
|
|
||||||
|
// Fehlendes 'name' Feld
|
||||||
|
let nameErrGefangen = false;
|
||||||
|
try {
|
||||||
|
parseBenutzerJson('{"alter": 30}');
|
||||||
|
} catch (err) {
|
||||||
|
nameErrGefangen = true;
|
||||||
|
assert.ok(err instanceof ValidierungsFehler, "Fehlendes 'name' muss ValidierungsFehler werfen");
|
||||||
|
assert.strictEqual(err.feld, "name", "Fehlerfeld sollte 'name' sein");
|
||||||
|
}
|
||||||
|
assert.ok(nameErrGefangen, "Objekt ohne 'name' muss abgelehnt werden");
|
||||||
|
|
||||||
|
// ==========================================
|
||||||
|
// 4. Tests für pruefeTransaktion
|
||||||
|
// ==========================================
|
||||||
|
assert.strictEqual(pruefeTransaktion(500, 200), 300, "500 - 200 sollte Saldo 300 ergeben");
|
||||||
|
assert.strictEqual(pruefeTransaktion(100, 100), 0, "100 - 100 sollte Saldo 0 ergeben");
|
||||||
|
|
||||||
|
// Nicht genügend Guthaben
|
||||||
|
let guthabenErrGefangen = false;
|
||||||
|
try {
|
||||||
|
pruefeTransaktion(50, 120);
|
||||||
|
} catch (err) {
|
||||||
|
guthabenErrGefangen = true;
|
||||||
|
assert.ok(err instanceof GuthabenFehler, "Überziehung muss GuthabenFehler werfen");
|
||||||
|
assert.strictEqual(err.saldo, 50, "GuthabenFehler.saldo muss 50 sein");
|
||||||
|
assert.strictEqual(err.betrag, 120, "GuthabenFehler.betrag muss 120 sein");
|
||||||
|
}
|
||||||
|
assert.ok(guthabenErrGefangen, "pruefeTransaktion bei Saldo < Betrag muss abbrechen");
|
||||||
|
|
||||||
|
// Negativer oder ungültiger Abhebebetrag
|
||||||
|
let ungueltigerBetragGefangen = false;
|
||||||
|
try {
|
||||||
|
pruefeTransaktion(500, -20);
|
||||||
|
} catch (err) {
|
||||||
|
ungueltigerBetragGefangen = true;
|
||||||
|
assert.ok(err instanceof ValidierungsFehler, "Negativer Betrag muss ValidierungsFehler werfen");
|
||||||
|
assert.strictEqual(err.feld, "abhebeBetrag", "Fehlerfeld sollte 'abhebeBetrag' sein");
|
||||||
|
}
|
||||||
|
assert.ok(ungueltigerBetragGefangen, "Negativer Betrag muss ValidierungsFehler werfen");
|
||||||
|
|
||||||
|
console.log("✅ Alle Tests in JS 08 (Fehlerbehandlung & Debugging) erfolgreich bestanden!");
|
||||||
|
|
@ -152,9 +152,9 @@ saveBtn.addEventListener("click", (event) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=04_dom_und_events" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=javascript&track=track_1_grundlagen&chapter=09_dom_und_events" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../03_objekte_und_destructuring/index.html" class="btn btn-secondary">← Zurück zu JS 03</a>
|
<a href="../05_objekte_und_destructuring/index.html" class="btn btn-secondary">← Zurück zu JS 05</a>
|
||||||
<a href="../05_callbacks_und_promises/index.html" class="btn btn-secondary">Weiter zu JS 05 →</a>
|
<a href="../11_callbacks_und_promises/index.html" class="btn btn-secondary">Weiter zu JS 07 →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
113
courses/javascript/10_web_storage_und_json/README.md
Normal file
113
courses/javascript/10_web_storage_und_json/README.md
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
# JS 10: Web Storage & JSON-Serialisierung 💾
|
||||||
|
|
||||||
|
Willkommen zu **Modul 10** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
In modernen Web-Anwendungen müssen Einstellungen, Warenkörbe, Spielstände oder Formulardaten clientseitig im Browser gespeichert werden – auch wenn der Benutzer die Seite neu lädt oder den Browser schließt.
|
||||||
|
|
||||||
|
Die **Web Storage API** (`localStorage` und `sessionStorage`) in Kombination mit **JSON-Serialisierung** (`JSON.stringify` & `JSON.parse`) bildet das Fundament für clientseitige Datenpersistenz.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. localStorage vs. sessionStorage
|
||||||
|
Der Browser stellt zwei getrennte Key-Value-Speicher zur Verfügung:
|
||||||
|
|
||||||
|
| Eigenschaft | `localStorage` | `sessionStorage` |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **Lebensdauer** | Dauerhaft (bleibt nach Browser-Neustart erhalten) | Bis der Browser-Tab geschlossen wird |
|
||||||
|
| **Gültigkeitsbereich** | Same-Origin (gleiches Protokoll, Domain, Port) | Nur im aktuellen Browser-Tab / Fenster |
|
||||||
|
| **Speicherkapazität** | Typisch ca. 5 – 10 MB pro Origin | Typisch ca. 5 MB pro Tab |
|
||||||
|
| **Typischer Nutzen** | Theme-Wahl, Highscores, Offline-Caches | Schrittweises Formular, temporärer Session-State |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Die Standard-Methoden der Storage API
|
||||||
|
Beide Speicher bieten dieselbe intuitive Schnittstelle:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Wert speichern (Key und Value sind immer Strings!):
|
||||||
|
localStorage.setItem("benutzerName", "Ada Lovelace");
|
||||||
|
|
||||||
|
// 2. Wert abrufen (liefert null, wenn der Key nicht existiert):
|
||||||
|
const name = localStorage.getItem("benutzerName"); // "Ada Lovelace"
|
||||||
|
const fehlt = localStorage.getItem("unbekannt"); // null
|
||||||
|
|
||||||
|
// 3. Einzelnen Eintrag löschen:
|
||||||
|
localStorage.removeItem("benutzerName");
|
||||||
|
|
||||||
|
// 4. Gesamten Speicher der Domain leeren:
|
||||||
|
localStorage.clear();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Komplexe Datentypen speichern mit JSON
|
||||||
|
Da die Web Storage API **ausschließlich Strings** speichert, führen Objekte und Arrays ohne Serialisierung zu dem gefürchteten `"[object Object]"`-Fehler!
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const spieler = { name: "Mario", punkte: 1200, items: ["Pilz", "Stern"] };
|
||||||
|
|
||||||
|
// ❌ FALSCH: Speichert nur den String "[object Object]"
|
||||||
|
localStorage.setItem("player", spieler);
|
||||||
|
|
||||||
|
// ✅ RICHTIG: In JSON-String serialisieren
|
||||||
|
const jsonString = JSON.stringify(spieler);
|
||||||
|
localStorage.setItem("player", jsonString);
|
||||||
|
|
||||||
|
// 📥 Beim Laden: Von JSON zurück in ein JavaScript-Objekt parsen
|
||||||
|
const geladenerString = localStorage.getItem("player");
|
||||||
|
if (geladenerString) {
|
||||||
|
const parsedPlayer = JSON.parse(geladenerString);
|
||||||
|
console.log(parsedPlayer.punkte); // 1200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D. Robuste Fehlerbehandlung (Defektes JSON & Quota Exceeded)
|
||||||
|
In Produktionscode können zwei Fehler auftreten:
|
||||||
|
1. **Defektes JSON im Storage:** Ein anderer Scriptteil oder eine Drittanbieter-Erweiterung hat ungültigen Text hinterlegt.
|
||||||
|
2. **QuotaExceededError:** Der Speicherplatz (5 MB) ist voll.
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
function sicheresLaden(key, fallback) {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
return raw !== null ? JSON.parse(raw) : fallback;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Warnung: JSON für ${key} beschädigt, nutze Fallback:`, err);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔐 Die didaktische Analogie: "Das Schließfach & das Reisetagebuch"
|
||||||
|
|
||||||
|
- **`localStorage` ist das Bankschließfach:**
|
||||||
|
Du legst deinen Reisepass (Benutzerprofil) oder deine Trophäen (Highscore) hinein. Selbst wenn du nach einem Jahr wiederkommst (Browser-Neustart), liegt alles genau so im Schließfach wie du es hinterlassen hast.
|
||||||
|
- **`sessionStorage` ist das Reisetagebuch der aktuellen Tour:**
|
||||||
|
Du schreibst deine Tagesnotizen hinein, solange du im Zug sitzt (aktueller Tab). Sobald du aussteigst und nach Hause gehst (Tab schließen), beginnt die nächste Reise mit einem frischen, leeren Notizblock.
|
||||||
|
- **`JSON.stringify` & `JSON.parse` sind Vakuumierer & Entpacker:**
|
||||||
|
Du kannst keinen dreidimensionalen Koffer (JavaScript-Objekt mit Methoden & Referenzen) durch den flachen Schließfachschlitz schieben. Du packst ihn flach zusammen (`stringify`), schiebst den Umschlag hinein und packst ihn beim Herausholen wieder vollständig aus (`parse`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1:** `speichereDaten(schluessel, daten, storageMock)`
|
||||||
|
Validiere den Schlüssel, serialisiere beliebige Werte mit `JSON.stringify` und fange Speicherfehler ab.
|
||||||
|
2. **TODO 2:** `ladeDaten(schluessel, standardWert = null, storageMock)`
|
||||||
|
Lade Daten mit `getItem`, behandle fehlende Einträge sowie `SyntaxError` bei defektem JSON mit `try/catch` und liefere den `standardWert` zurück.
|
||||||
|
3. **TODO 3:** `loescheDaten(schluessel, storageMock)`
|
||||||
|
Entferne den Schlüssel sauber aus dem Storage mit `removeItem`.
|
||||||
|
4. **TODO 4:** `aktualisiereHighscore(spielerName, punkte, storageMock)`
|
||||||
|
Verwalte eine Bestenliste unter dem Key `"highscores"`. Füge den neuen Spieler ein, sortiere absteigend nach Punkten, kürze auf die Top 10 und speichere die Liste persistent ab.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE oder führe die Unittests aus. Sobald alle 16 Tests grün sind, hast du clientseitige Persistenz und JSON-Verarbeitung gemeistert!
|
||||||
113
courses/javascript/10_web_storage_und_json/aufgabe.js
Normal file
113
courses/javascript/10_web_storage_und_json/aufgabe.js
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 10: WEB STORAGE & JSON SERIALISIERUNG 🌐
|
||||||
|
* ===============================================
|
||||||
|
* In diesem Modul lernst du, wie Daten dauerhaft im Browser
|
||||||
|
* (localStorage & sessionStorage) als JSON persistiert werden.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hilfsfunktion zum Auflösen des Speichers (Echter Storage oder Mock)
|
||||||
|
* @param {Object} [storageMock=null]
|
||||||
|
* @returns {Storage | Object | null}
|
||||||
|
*/
|
||||||
|
function ermittleStorage(storageMock = null) {
|
||||||
|
if (storageMock) return storageMock;
|
||||||
|
if (typeof localStorage !== "undefined") return localStorage;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1): speichereDaten(schluessel, daten, storageMock)
|
||||||
|
* Serialisiert Daten zu einem JSON-String und speichert sie im Web Storage.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - schluessel: string (muss ein nicht-leerer String sein, sonst TypeError)
|
||||||
|
* - daten: Beliebige serialisierbare Daten (Objekt, Array, Zahl, Boolean, String)
|
||||||
|
* - storageMock: Optionaler Storage-Ersatz für isolierte Tests
|
||||||
|
* - Serialisiert `daten` mit JSON.stringify()
|
||||||
|
* - Speichert mit storage.setItem(schluessel, jsonString)
|
||||||
|
* - Fängt Speicherfehler (z.B. QuotaExceededError) ab und gibt false zurück
|
||||||
|
* - Gibt bei Erfolg true zurück.
|
||||||
|
*
|
||||||
|
* @param {string} schluessel
|
||||||
|
* @param {any} daten
|
||||||
|
* @param {Object} [storageMock=null]
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function speichereDaten(schluessel, daten, storageMock = null) {
|
||||||
|
// TODO: Schlüssel validieren, JSON.stringify verwenden und in Storage schreiben
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 2 (TODO 2): ladeDaten(schluessel, standardWert, storageMock)
|
||||||
|
* Lädt Daten aus dem Storage und deserialisiert sie sicher von JSON.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - schluessel: string
|
||||||
|
* - standardWert: Rückfallwert (Default: null), falls Schlüssel nicht existiert oder defekt ist
|
||||||
|
* - storageMock: Optionaler Storage-Ersatz
|
||||||
|
* - Liest den Rohwert mit storage.getItem(schluessel)
|
||||||
|
* - Falls der Schlüssel nicht existiert (getItem liefert null), gib `standardWert` zurück.
|
||||||
|
* - Versucht JSON.parse() auf den Rohdaten.
|
||||||
|
* - Bei SyntaxError (defektes JSON im Storage) fange den Fehler ab und gib `standardWert` zurück.
|
||||||
|
* - Gibt die deserialisierten Daten zurück.
|
||||||
|
*
|
||||||
|
* @param {string} schluessel
|
||||||
|
* @param {any} [standardWert=null]
|
||||||
|
* @param {Object} [storageMock=null]
|
||||||
|
* @returns {any}
|
||||||
|
*/
|
||||||
|
function ladeDaten(schluessel, standardWert = null, storageMock = null) {
|
||||||
|
// TODO: storage.getItem abrufen, JSON.parse mit try/catch ausführen
|
||||||
|
return standardWert;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 3 (TODO 3): loescheDaten(schluessel, storageMock)
|
||||||
|
* Entfernt einen Eintrag aus dem Storage.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - schluessel: string (muss ein gültiger String sein)
|
||||||
|
* - Ruft storage.removeItem(schluessel) auf.
|
||||||
|
* - Gibt true zurück wenn erfolgreich, sonst false bei ungültigem Schlüssel.
|
||||||
|
*
|
||||||
|
* @param {string} schluessel
|
||||||
|
* @param {Object} [storageMock=null]
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function loescheDaten(schluessel, storageMock = null) {
|
||||||
|
// TODO: storage.removeItem ausführen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} HighscoreEintrag
|
||||||
|
* @property {string} name - Name des Spielers
|
||||||
|
* @property {number} punkte - Erzielte Punktzahl
|
||||||
|
* @property {string} datum - ISO-Datumsstring
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 4 (TODO 4): aktualisiereHighscore(spielerName, punkte, storageMock)
|
||||||
|
* Verwaltet eine Highscore-Bestenliste unter dem Schlüssel "highscores".
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - spielerName: string (nach .trim() nicht leer, sonst TypeError werfen)
|
||||||
|
* - punkte: number (muss Zahl >= 0 und !isNaN sein, sonst TypeError werfen)
|
||||||
|
* - Lädt bestehende Highscores mit ladeDaten("highscores", [], storageMock).
|
||||||
|
* - Erstellt neuen Eintrag: { name: spielerName.trim(), punkte: punkte, datum: new Date().toISOString() }
|
||||||
|
* - Sortiert die Highscores absteigend nach `punkte` (höchste Punktzahl auf Index 0).
|
||||||
|
* - Begrenzt die Liste auf die Top 10 Einträge.
|
||||||
|
* - Speichert die aktualisierte Liste mit speichereDaten("highscores", liste, storageMock).
|
||||||
|
* - Gibt das sortierte Array der HighscoreEintraege zurück.
|
||||||
|
*
|
||||||
|
* @param {string} spielerName
|
||||||
|
* @param {number} punkte
|
||||||
|
* @param {Object} [storageMock=null]
|
||||||
|
* @returns {HighscoreEintrag[]}
|
||||||
|
*/
|
||||||
|
function aktualisiereHighscore(spielerName, punkte, storageMock = null) {
|
||||||
|
// TODO: Highscores laden, neuen Eintrag einfügen, sortieren, begrenzen und speichern
|
||||||
|
return [];
|
||||||
|
}
|
||||||
195
courses/javascript/10_web_storage_und_json/index.html
Normal file
195
courses/javascript/10_web_storage_und_json/index.html
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 10: Web Storage & JSON-Serialisierung</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">💾 Clientseitige Persistenz & JSON</div>
|
||||||
|
<h1>JS 10: Web Storage & JSON-Serialisierung</h1>
|
||||||
|
<p>Lerne clientseitige Datenhaltung mit <code>localStorage</code>, <code>sessionStorage</code> und robuster JSON-Verarbeitung für Offline-First Web-Apps.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 📂 Schnellzugriff & Download -->
|
||||||
|
<div style="background: #ffffff; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; box-shadow: var(--shadow-sm);">
|
||||||
|
<div style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>📂 Dateien dieser Einheit:</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<a href="aufgabe.js" class="btn" style="padding: 6px 14px; font-size: 0.88rem; background: var(--primary);" download>📄 aufgabe.js (Startercode)</a>
|
||||||
|
<a href="test_aufgabe.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🧪 test_aufgabe.js (Tests)</a>
|
||||||
|
<a href="musterloesung.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>💡 musterloesung.js (Lösung)</a>
|
||||||
|
<a href="README.md" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;">📖 README.md</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. THEORIE: LOCALSTORAGE VS SESSIONSTORAGE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. 📦 localStorage vs. sessionStorage</h2>
|
||||||
|
<p>Moderne Browser stellen zwei Schlüssel-Wert-Speicher zur Verfügung, die rein clientseitig ohne Server-Datenbank arbeiten:</p>
|
||||||
|
|
||||||
|
<div style="overflow-x: auto; margin: 16px 0;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Merkmal</th>
|
||||||
|
<th style="padding: 10px;"><code>localStorage</code></th>
|
||||||
|
<th style="padding: 10px;"><code>sessionStorage</code></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px; font-weight: 600;">Lebensdauer</td>
|
||||||
|
<td style="padding: 10px;">Dauerhaft (überlebt Browser-Neustarts)</td>
|
||||||
|
<td style="padding: 10px;">Nur für die Dauer des Browser-Tabs</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px; font-weight: 600;">Scope</td>
|
||||||
|
<td style="padding: 10px;">Same-Origin (über alle Tabs derselben Domain)</td>
|
||||||
|
<td style="padding: 10px;">Isoliert auf den einzelnen Tab</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px; font-weight: 600;">Speicherplatz</td>
|
||||||
|
<td style="padding: 10px;">ca. 5–10 MB pro Origin</td>
|
||||||
|
<td style="padding: 10px;">ca. 5 MB pro Tab</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px; font-weight: 600;">Typischer Einsatz</td>
|
||||||
|
<td style="padding: 10px;">Theme (Dark Mode), Highscores, Entwürfe</td>
|
||||||
|
<td style="padding: 10px;">Mehrschritt-Formulare, temporärer Tab-Status</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🔐 Didaktische Analogie: Das Schließfach & das Reisetagebuch</div>
|
||||||
|
<p><strong>localStorage</strong> ist wie ein <strong>Bankschließfach</strong>: Was du dort ablegst, wartet auch nach Jahren treu auf dich. <strong>sessionStorage</strong> ist wie ein <strong>Reisetagebuch im Zugabteil</strong>: Sobald du den Zug verlässt (Tab schließen), verbleibt es im alten Zug und eine neue Fahrt beginnt mit leeren Blättern.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: METHODEN & JSON SERIALISIERUNG -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. 🔄 JSON-Serialisierung komplexer Datenstrukturen</h2>
|
||||||
|
<p>Da Web Storage <strong>nur Strings</strong> akzeptiert, müssen Objekte und Arrays vor dem Speichern mit <code>JSON.stringify()</code> serialisiert und beim Laden mit <code>JSON.parse()</code> deserialisiert werden:</p>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 1. Objekt serialisieren & speichern:
|
||||||
|
const benutzer = { id: 101, name: "Ada", skills: ["JS", "TS"], aktiv: true };
|
||||||
|
localStorage.setItem("user_profile", JSON.stringify(benutzer));
|
||||||
|
|
||||||
|
// 🎯 2. Laden & Deserialisieren:
|
||||||
|
const raw = localStorage.getItem("user_profile");
|
||||||
|
if (raw) {
|
||||||
|
const geladenerBenutzer = JSON.parse(raw);
|
||||||
|
console.log(geladenerBenutzer.skills[0]); // "JS"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎯 3. Einzelnen Schlüssel entfernen:
|
||||||
|
localStorage.removeItem("user_profile");</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-tipp">
|
||||||
|
<div class="box-title">💡 Warum kein direktes Speichern von Objekten?</div>
|
||||||
|
<p>Wenn du <code>localStorage.setItem("obj", { a: 1 })</code> ohne <code>JSON.stringify</code> aufrufst, ruft JavaScript automatisch <code>.toString()</code> auf. Im Storage landet dann der unbrauchbare String <code>"[object Object]"</code>!</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. THEORIE: FEHLERBEHANDLUNG -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>3. 🛡️ Robuste Fehlerbehandlung mit <code>try / catch</code></h2>
|
||||||
|
<p>Im professionellen Umfeld kann der Inhalt von Storage durch manuelle Benutzer-Eingaben oder veraltete Formate korrupt sein. Defektes JSON wirft beim Parsen einen <code>SyntaxError</code>:</p>
|
||||||
|
|
||||||
|
<pre><code>function ladeEinstellungenSicher(key, standardWert) {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key);
|
||||||
|
return raw !== null ? JSON.parse(raw) : standardWert;
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`Defektes JSON bei Schlüssel "${key}", nutze Standard-Fallback.`, err);
|
||||||
|
return standardWert;
|
||||||
|
}
|
||||||
|
}</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 4. Cheat-Sheet: Web Storage API</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Befehl</th>
|
||||||
|
<th style="padding: 10px;">Signatur</th>
|
||||||
|
<th style="padding: 10px;">Beschreibung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>setItem</code></td>
|
||||||
|
<td style="padding: 10px;"><code>storage.setItem(key, value)</code></td>
|
||||||
|
<td style="padding: 10px;">Schreibt Schlüssel & Wert (als String)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>getItem</code></td>
|
||||||
|
<td style="padding: 10px;"><code>storage.getItem(key)</code></td>
|
||||||
|
<td style="padding: 10px;">Liest Wert oder liefert <code>null</code></td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>removeItem</code></td>
|
||||||
|
<td style="padding: 10px;"><code>storage.removeItem(key)</code></td>
|
||||||
|
<td style="padding: 10px;">Löscht den Schlüssel aus dem Speicher</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>clear</code></td>
|
||||||
|
<td style="padding: 10px;"><code>storage.clear()</code></td>
|
||||||
|
<td style="padding: 10px;">Leert den gesamten Speicher der Domain</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>JSON.stringify/parse</code></td>
|
||||||
|
<td style="padding: 10px;"><code>JSON.stringify(obj) / JSON.parse(str)</code></td>
|
||||||
|
<td style="padding: 10px;">Objekt-Serialisierung & Deserialisierung</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 5. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 5. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>speichereDaten(schluessel, daten, storageMock)</code> – Schlüssel validieren, <code>JSON.stringify</code> ausführen und Fehler abfangen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>ladeDaten(schluessel, standardWert, storageMock)</code> – <code>getItem</code> laden, <code>JSON.parse</code> im <code>try/catch</code> ausführen mit Fallback.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>loescheDaten(schluessel, storageMock)</code> – Eintrag sauber mit <code>removeItem</code> entfernen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>aktualisiereHighscore(spielerName, punkte, storageMock)</code> – Highscores laden, sortieren (absteigend), Top 10 begrenzen & speichern.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_3_storage_und_typescript&chapter=10_web_storage_und_json" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../09_dom_und_events/index.html" class="btn btn-secondary">← Zurück zu JS 09 (DOM & Events)</a>
|
||||||
|
<a href="../11_callbacks_und_promises/index.html" class="btn btn-secondary">Weiter zu JS 11 (Callbacks & Promises) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../../assets/ui_enhancements.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
85
courses/javascript/10_web_storage_und_json/musterloesung.js
Normal file
85
courses/javascript/10_web_storage_und_json/musterloesung.js
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: JS 10 – WEB STORAGE & JSON SERIALISIERUNG 🌟
|
||||||
|
* =============================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function ermittleStorage(storageMock = null) {
|
||||||
|
if (storageMock) return storageMock;
|
||||||
|
if (typeof localStorage !== "undefined") return localStorage;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function speichereDaten(schluessel, daten, storageMock = null) {
|
||||||
|
if (typeof schluessel !== "string" || schluessel.trim().length === 0) {
|
||||||
|
throw new TypeError("Schlüssel muss ein nicht-leerer String sein");
|
||||||
|
}
|
||||||
|
const storage = ermittleStorage(storageMock);
|
||||||
|
if (!storage) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const jsonString = JSON.stringify(daten);
|
||||||
|
storage.setItem(schluessel, jsonString);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ladeDaten(schluessel, standardWert = null, storageMock = null) {
|
||||||
|
if (typeof schluessel !== "string") {
|
||||||
|
return standardWert;
|
||||||
|
}
|
||||||
|
const storage = ermittleStorage(storageMock);
|
||||||
|
if (!storage) return standardWert;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const raw = storage.getItem(schluessel);
|
||||||
|
if (raw === null || raw === undefined) {
|
||||||
|
return standardWert;
|
||||||
|
}
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (error) {
|
||||||
|
return standardWert;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loescheDaten(schluessel, storageMock = null) {
|
||||||
|
if (typeof schluessel !== "string" || schluessel.trim().length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const storage = ermittleStorage(storageMock);
|
||||||
|
if (!storage) return false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
storage.removeItem(schluessel);
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function aktualisiereHighscore(spielerName, punkte, storageMock = null) {
|
||||||
|
if (typeof spielerName !== "string" || spielerName.trim().length === 0) {
|
||||||
|
throw new TypeError("Spielername muss ein nicht-leerer String sein");
|
||||||
|
}
|
||||||
|
if (typeof punkte !== "number" || isNaN(punkte) || punkte < 0) {
|
||||||
|
throw new TypeError("Punkte müssen eine gültige Zahl >= 0 sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestehendeHighscores = ladeDaten("highscores", [], storageMock);
|
||||||
|
const liste = Array.isArray(bestehendeHighscores) ? [...bestehendeHighscores] : [];
|
||||||
|
|
||||||
|
const neuerEintrag = {
|
||||||
|
name: spielerName.trim(),
|
||||||
|
punkte: punkte,
|
||||||
|
datum: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
liste.push(neuerEintrag);
|
||||||
|
liste.sort((a, b) => b.punkte - a.punkte);
|
||||||
|
|
||||||
|
const top10 = liste.slice(0, 10);
|
||||||
|
speichereDaten("highscores", top10, storageMock);
|
||||||
|
|
||||||
|
return top10;
|
||||||
|
}
|
||||||
153
courses/javascript/10_web_storage_und_json/test_aufgabe.js
Normal file
153
courses/javascript/10_web_storage_und_json/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
/**
|
||||||
|
* 🧪 Testsuite für JS 10: Web Storage & JSON Serialisierung
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createMockStorage() {
|
||||||
|
const store = new Map();
|
||||||
|
return {
|
||||||
|
getItem(key) {
|
||||||
|
return store.has(String(key)) ? store.get(String(key)) : null;
|
||||||
|
},
|
||||||
|
setItem(key, value) {
|
||||||
|
store.set(String(key), String(value));
|
||||||
|
},
|
||||||
|
removeItem(key) {
|
||||||
|
store.delete(String(key));
|
||||||
|
},
|
||||||
|
clear() {
|
||||||
|
store.clear();
|
||||||
|
},
|
||||||
|
get length() {
|
||||||
|
return store.size;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertThrows(fn, msg) {
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
assert.ok(threw, msg || "Erwartete Exception wurde nicht geworfen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 1. Tests für speichereDaten(schluessel, daten, storageMock)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const mock1 = createMockStorage();
|
||||||
|
const benutzerObjekt = { id: 42, name: "Ada Lovelace", isAdmin: true, rollen: ["dev", "admin"] };
|
||||||
|
|
||||||
|
const gespeichert = speichereDaten("user_ada", benutzerObjekt, mock1);
|
||||||
|
assert.strictEqual(gespeichert, true, "speichereDaten sollte bei Erfolg true zurückgeben");
|
||||||
|
|
||||||
|
const gespeicherterRawString = mock1.getItem("user_ada");
|
||||||
|
assert.ok(gespeicherterRawString !== null, "Der Wert muss im Storage vorhanden sein");
|
||||||
|
assert.strictEqual(
|
||||||
|
gespeicherterRawString,
|
||||||
|
JSON.stringify(benutzerObjekt),
|
||||||
|
"Die Daten müssen als gültiger JSON-String serialisiert abgelegt werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Zahl und Boolean speichern
|
||||||
|
speichereDaten("app_volume", 85, mock1);
|
||||||
|
assert.strictEqual(mock1.getItem("app_volume"), "85", "Zahlen müssen als JSON-String gespeichert werden");
|
||||||
|
|
||||||
|
speichereDaten("dark_mode", true, mock1);
|
||||||
|
assert.strictEqual(mock1.getItem("dark_mode"), "true", "Booleans müssen als JSON-String gespeichert werden");
|
||||||
|
|
||||||
|
// Validierung: Leerer Schlüssel muss TypeError werfen
|
||||||
|
assertThrows(() => speichereDaten("", { a: 1 }, mock1), "Leerer Schlüssel muss TypeError werfen");
|
||||||
|
assertThrows(() => speichereDaten(null, { a: 1 }, mock1), "null als Schlüssel muss TypeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 2. Tests für ladeDaten(schluessel, standardWert, storageMock)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const mock2 = createMockStorage();
|
||||||
|
mock2.setItem("einstellungen", JSON.stringify({ theme: "dark", fontSize: 16 }));
|
||||||
|
|
||||||
|
const geladeneEinstellungen = ladeDaten("einstellungen", {}, mock2);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
geladeneEinstellungen,
|
||||||
|
{ theme: "dark", fontSize: 16 },
|
||||||
|
"ladeDaten() sollte das JSON-Objekt korrekt rekonstruieren"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback bei nicht existierendem Schlüssel
|
||||||
|
const nichtVorhanden = ladeDaten("unbekannter_schluessel", "Standard-Wert", mock2);
|
||||||
|
assert.strictEqual(nichtVorhanden, "Standard-Wert", "Nicht existierender Schlüssel liefert standardWert");
|
||||||
|
|
||||||
|
const nichtVorhandenNull = ladeDaten("gibts_nicht", null, mock2);
|
||||||
|
assert.strictEqual(nichtVorhandenNull, null, "Default standardWert ist null");
|
||||||
|
|
||||||
|
// Fehlerbehandlung: Defektes JSON im Storage abfangen
|
||||||
|
mock2.setItem("defekter_eintrag", "{ name: 'Fehlerhaftes JSON ohne Anfuehrungszeichen'");
|
||||||
|
const geladenDefekt = ladeDaten("defekter_eintrag", { repariert: true }, mock2);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
geladenDefekt,
|
||||||
|
{ repariert: true },
|
||||||
|
"Bei SyntaxError in JSON.parse muss standardWert zurückgegeben werden"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 3. Tests für loescheDaten(schluessel, storageMock)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const mock3 = createMockStorage();
|
||||||
|
mock3.setItem("session_token", "secret_12345");
|
||||||
|
assert.strictEqual(mock3.getItem("session_token"), "secret_12345");
|
||||||
|
|
||||||
|
const geloescht = loescheDaten("session_token", mock3);
|
||||||
|
assert.strictEqual(geloescht, true, "loescheDaten sollte true zurückgeben");
|
||||||
|
assert.strictEqual(mock3.getItem("session_token"), null, "Eintrag muss nach dem Löschen gelöscht sein");
|
||||||
|
|
||||||
|
const loescheUngueltig = loescheDaten("", mock3);
|
||||||
|
assert.strictEqual(loescheUngueltig, false, "Ungültiger Schlüssel liefert false");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 4. Tests für aktualisiereHighscore(spielerName, punkte, storageMock)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const mock4 = createMockStorage();
|
||||||
|
|
||||||
|
// Erster Eintrag
|
||||||
|
const score1 = aktualisiereHighscore("Alice", 1500, mock4);
|
||||||
|
assert.strictEqual(score1.length, 1, "Erste Bestenliste hat 1 Eintrag");
|
||||||
|
assert.strictEqual(score1[0].name, "Alice");
|
||||||
|
assert.strictEqual(score1[0].punkte, 1500);
|
||||||
|
assert.ok(typeof score1[0].datum === "string", "Eintrag muss ein ISO-Datum haben");
|
||||||
|
|
||||||
|
// Zweiter Eintrag mit höherer Punktzahl -> Muss an Index 0 stehen
|
||||||
|
const score2 = aktualisiereHighscore("Bob", 2800, mock4);
|
||||||
|
assert.strictEqual(score2.length, 2, "Bestenliste hat nun 2 Einträge");
|
||||||
|
assert.strictEqual(score2[0].name, "Bob", "Höhere Punktzahl muss an erster Stelle stehen");
|
||||||
|
assert.strictEqual(score2[0].punkte, 2800);
|
||||||
|
assert.strictEqual(score2[1].name, "Alice");
|
||||||
|
assert.strictEqual(score2[1].punkte, 1500);
|
||||||
|
|
||||||
|
// Dritter Eintrag mit mittlerer Punktzahl
|
||||||
|
const score3 = aktualisiereHighscore(" Charlie ", 2100, mock4);
|
||||||
|
assert.strictEqual(score3.length, 3);
|
||||||
|
assert.strictEqual(score3[0].name, "Bob");
|
||||||
|
assert.strictEqual(score3[1].name, "Charlie", "Name muss mit .trim() bereinigt werden");
|
||||||
|
assert.strictEqual(score3[1].punkte, 2100);
|
||||||
|
assert.strictEqual(score3[2].name, "Alice");
|
||||||
|
|
||||||
|
// Prüfung ob Bestenliste im Storage persistiert wurde
|
||||||
|
const persistierteScores = ladeDaten("highscores", [], mock4);
|
||||||
|
assert.strictEqual(persistierteScores.length, 3, "Highscores müssen im Storage gespeichert sein");
|
||||||
|
assert.strictEqual(persistierteScores[0].name, "Bob");
|
||||||
|
|
||||||
|
// Test Begrenzung auf Top 10
|
||||||
|
for (let i = 1; i <= 15; i++) {
|
||||||
|
aktualisiereHighscore(`Player_${i}`, i * 100, mock4);
|
||||||
|
}
|
||||||
|
const topScores = ladeDaten("highscores", [], mock4);
|
||||||
|
assert.strictEqual(topScores.length, 10, "Highscore-Liste darf maximal 10 Einträge umfassen");
|
||||||
|
assert.ok(topScores[0].punkte >= topScores[1].punkte, "Bestenliste muss absteigend sortiert sein");
|
||||||
|
|
||||||
|
// Validierungstests
|
||||||
|
assertThrows(() => aktualisiereHighscore("", 500, mock4), "Leerer Spielername muss TypeError werfen");
|
||||||
|
assertThrows(() => aktualisiereHighscore("Dave", -10, mock4), "Negative Punkte müssen TypeError werfen");
|
||||||
|
assertThrows(() => aktualisiereHighscore("Dave", NaN, mock4), "NaN als Punktzahl muss TypeError werfen");
|
||||||
|
|
||||||
|
console.log("✅ Alle 16 Tests für JS 10 (Web Storage & JSON Serialisierung) erfolgreich bestanden!");
|
||||||
|
|
@ -193,7 +193,7 @@ Promise.all([quelleA, quelleB])
|
||||||
|
|
||||||
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=05_callbacks_und_promises" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=05_callbacks_und_promises" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
||||||
<a href="../06_async_await_und_fetch/index.html" class="btn" style="background: #4f46e5; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu Modul 06 →</a>
|
<a href="../12_async_await_und_fetch/index.html" class="btn" style="background: #4f46e5; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">Weiter zu Modul 06 →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
|
<div style="margin-top: 24px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
<a href="../05_callbacks_und_promises/index.html" class="btn" style="background: #64748b; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">← Zurück zu Modul 05</a>
|
<a href="../11_callbacks_und_promises/index.html" class="btn" style="background: #64748b; color: #fff; font-weight: 600; text-decoration: none; padding: 10px 20px; border-radius: 6px;">← Zurück zu Modul 05</a>
|
||||||
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=06_async_await_und_fetch" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
<a href="../../../workspace.html?course=javascript&track=track_2_async_und_apis&chapter=06_async_await_und_fetch" class="btn" style="background: #eab308; color: #000; font-weight: 700; text-decoration: none; padding: 10px 20px; border-radius: 6px;">💻 In Web-IDE öffnen →</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
107
courses/javascript/13_es_module_und_architektur/README.md
Normal file
107
courses/javascript/13_es_module_und_architektur/README.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
# JS 13: ES Module & Clean Architecture 🧱
|
||||||
|
|
||||||
|
Willkommen zu **Modul 13** des JavaScript-Kurses!
|
||||||
|
|
||||||
|
Große Softwareprojekte scheitern selten an einzelnen Algorithmen – sie scheitern an unübersichtlichem "Spaghetti-Code", bei dem alles mit allem verknüpft ist.
|
||||||
|
|
||||||
|
In diesem Modul lernst du das offizielle JavaScript-Modulsystem (**ES Modules**) sowie grundlegende Architekturprinzipien wie **Separation of Concerns (SoC)** und **Dependency Injection (DI)** kennen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. Named Exports vs. Default Exports
|
||||||
|
In ES6+ teilst du Code in eigenständige Dateien auf:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 📁 mathUtils.js (Named Exports)
|
||||||
|
export const PI = 3.1415926535;
|
||||||
|
|
||||||
|
export function addieren(a, b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function multiplizieren(a, b) {
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 📁 RechnungsEngine.js (Default Export)
|
||||||
|
export default class RechnungsEngine {
|
||||||
|
constructor() {
|
||||||
|
this.name = "Haupt-Engine";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Module importieren
|
||||||
|
Beim Importieren kannst du gezielte Teile oder das gesamte Modul einbinden:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 1. Gezielter Named Import:
|
||||||
|
import { addieren, PI } from "./mathUtils.js";
|
||||||
|
|
||||||
|
// 2. Default Import (freier Name wählbar):
|
||||||
|
import RechnungsEngine from "./RechnungsEngine.js";
|
||||||
|
|
||||||
|
// 3. Namespace Import (alles als Objekt bündeln):
|
||||||
|
import * as MathUtils from "./mathUtils.js";
|
||||||
|
console.log(MathUtils.addieren(5, 3));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Separation of Concerns (Trennung von Verantwortlichkeiten)
|
||||||
|
Saubere Software trennt verschiedene Aufgabenbereiche strikt:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────┐
|
||||||
|
│ RechnungsEngine (Controller) │
|
||||||
|
│ - Steuert den Ablauf │
|
||||||
|
│ - Kennt keine direkten UI-/DOM-Details │
|
||||||
|
└──────────────┬───────────────────────────┬──────────────┘
|
||||||
|
│ nutzt │ nutzt
|
||||||
|
▼ ▼
|
||||||
|
┌──────────────────────────────┐ ┌────────────────────────┐
|
||||||
|
│ MathModule (Logik & Mathe) │ │ FormatterModule (View) │
|
||||||
|
│ - Reines Rechnen │ │ - Währung ("1.250 €") │
|
||||||
|
│ - Keine Nebeneffekte │ │ - Datum ("2026-08-30") │
|
||||||
|
└──────────────────────────────┘ └────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
1. **Logikschicht (`MathModule`):** Rechnet präzise und wirft bei unzulässigen Werten (z.B. Division durch 0) kontrollierte Fehler.
|
||||||
|
2. **Präsentationsschicht (`FormatterModule`):** Bereitet Zahlen und Datumsangaben für Menschen lesbar auf (z.B. deutsches Währungsformat mit Komma und Tausendertrennzeichen).
|
||||||
|
3. **Orchestrierungsschicht (`RechnungsEngine`):** Verbindet Datenmodell und Schichten via **Dependency Injection** im Konstruktor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ Die didaktische Analogie: "Die modularen Legosteine & Container-Bauweise"
|
||||||
|
|
||||||
|
- **Monolithischer Code ist wie eine Statue aus einem einzigen Steinblock:**
|
||||||
|
Willst du einen Arm anpassen, bricht der ganze Steinblock auseinander.
|
||||||
|
- **Modulare Software ist wie moderne Frachtcontainer & Legosteine:**
|
||||||
|
- Jeder Container hat standardisierte Eckbeschläge (definierte Schnittstelle / Exports).
|
||||||
|
- Was im Container transportiert wird (interne Implementierung), ist dem Kran (Controller) egal.
|
||||||
|
- Du kannst den Motor (z.B. ein Mathe-Modul) jederzeit gegen einen Turbomotor (Mock-Objekt im Test) austauschen, solange die Stecker passen (**Dependency Injection**).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1:** `MathModule`
|
||||||
|
Implementiere `addieren`, `subtrahieren`, `multiplizieren`, `dividieren` (mit Schutz vor Division durch 0) und `runden`.
|
||||||
|
2. **TODO 2:** `FormatterModule`
|
||||||
|
Implementiere `formatiereWaehrung` (deutsches Format, z.B. `1.250,50 EUR`) und `formatiereDatumIso` (`YYYY-MM-DD`).
|
||||||
|
3. **TODO 3:** `RechnungsEngine.erstellePosition(bezeichnung, menge, einzelpreis)`
|
||||||
|
Validiere Eingaben, berechne den Gesamtpreis über `this.math` und gib die Position zurück.
|
||||||
|
4. **TODO 4:** `RechnungsEngine.berechneRechnung(positionen, steuersatzProzent)`
|
||||||
|
Summiere Netto, berechne Steuer und Brutto, und formatiere alle Ausgaben über `this.formatter`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE oder führe die Unittests aus. Sobald alle 20 Tests grün sind, hast du Modul-Architektur und saubere Code-Strukturierung gemeistert!
|
||||||
187
courses/javascript/13_es_module_und_architektur/aufgabe.js
Normal file
187
courses/javascript/13_es_module_und_architektur/aufgabe.js
Normal file
|
|
@ -0,0 +1,187 @@
|
||||||
|
/**
|
||||||
|
* 🌐 JS 13: ES MODULE & ARCHITEKTUR (CLEAN CODE & SEPARATION OF CONCERNS) 🌐
|
||||||
|
* =========================================================================
|
||||||
|
* In diesem Modul lernst du, wie modularer Code mit klarer Trennung von
|
||||||
|
* Geschäftslogik (Math), Präsentation (Formatter) und Controller (Engine)
|
||||||
|
* aufgebaut wird.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 1. MODUL: MathModule (Reine mathematische Geschäftslogik)
|
||||||
|
// ============================================================================
|
||||||
|
const MathModule = {
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1a): addieren(a, b)
|
||||||
|
* Addiert zwei Zahlen. Wirft TypeError falls a oder b keine gültige Zahl ist.
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
addieren(a, b) {
|
||||||
|
// TODO: Validierung & Addition
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1b): subtrahieren(a, b)
|
||||||
|
* Subtrahiert b von a. Wirft TypeError falls a oder b keine gültige Zahl ist.
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
subtrahieren(a, b) {
|
||||||
|
// TODO: Validierung & Subtraktion
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1c): multiplizieren(a, b)
|
||||||
|
* Multipliziert zwei Zahlen. Wirft TypeError falls a oder b keine gültige Zahl ist.
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
multiplizieren(a, b) {
|
||||||
|
// TODO: Validierung & Multiplikation
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1d): dividieren(a, b)
|
||||||
|
* Dividiert a durch b. Wirft TypeError bei ungültigen Zahlen und
|
||||||
|
* Error("Division durch 0 nicht erlaubt") falls b === 0 ist.
|
||||||
|
* @param {number} a
|
||||||
|
* @param {number} b
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
dividieren(a, b) {
|
||||||
|
// TODO: Validierung, 0-Prüfung & Division
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1e): runden(betrag, dezimalstellen = 2)
|
||||||
|
* Rundet kaufmännisch auf die übergebene Anzahl von Nachkommastellen.
|
||||||
|
* @param {number} betrag
|
||||||
|
* @param {number} [dezimalstellen=2]
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
|
runden(betrag, dezimalstellen = 2) {
|
||||||
|
// TODO: Kaufmännisches Runden
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 2. MODUL: FormatterModule (Präsentations- & Formatierungslogik)
|
||||||
|
// ============================================================================
|
||||||
|
const FormatterModule = {
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 2 (TODO 2a): formatiereWaehrung(betrag, waehrung = "EUR")
|
||||||
|
* Formatiert eine Zahl im deutschen Währungsformat mit 2 Nachkommastellen
|
||||||
|
* und Tausender-Trennzeichen:
|
||||||
|
* z.B. 1250.5 -> "1.250,50 EUR", 49.99 -> "49,99 EUR", 0 -> "0,00 EUR"
|
||||||
|
* Wirft TypeError falls betrag keine Zahl ist oder waehrung ungültig ist.
|
||||||
|
*
|
||||||
|
* @param {number} betrag
|
||||||
|
* @param {string} [waehrung="EUR"]
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatiereWaehrung(betrag, waehrung = "EUR") {
|
||||||
|
// TODO: Betrag runden, deutsches Format mit Komma und Tausender-Punkt erstellen
|
||||||
|
return "";
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 2 (TODO 2b): formatiereDatumIso(datum)
|
||||||
|
* Wandelt ein Date-Objekt oder einen Datums-String in das ISO-Format "YYYY-MM-DD" um.
|
||||||
|
* Wirft TypeError falls das Datum ungültig ist.
|
||||||
|
* z.B. new Date(2026, 7, 30) -> "2026-08-30"
|
||||||
|
*
|
||||||
|
* @param {Date | string | number} datum
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
formatiereDatumIso(datum) {
|
||||||
|
// TODO: Date-Objekt validieren und als YYYY-MM-DD formatieren
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 3. HAUPT-CONTROLLER: RechnungsEngine (Orchestrator mit Dependency Injection)
|
||||||
|
// ============================================================================
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Rechnungsposition
|
||||||
|
* @property {string} bezeichnung - Name des Artikels / der Dienstleistung
|
||||||
|
* @property {number} menge - Anzahl (muss > 0 sein)
|
||||||
|
* @property {number} einzelpreis - Preis pro Einheit (muss >= 0 sein)
|
||||||
|
* @property {number} gesamtpreis - menge * einzelpreis
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} Rechnung
|
||||||
|
* @property {Rechnungsposition[]} positionen
|
||||||
|
* @property {number} nettoSumme
|
||||||
|
* @property {number} steuersatzProzent
|
||||||
|
* @property {number} steuerBetrag
|
||||||
|
* @property {number} bruttoSumme
|
||||||
|
* @property {string} nettoFormatiert
|
||||||
|
* @property {string} steuerFormatiert
|
||||||
|
* @property {string} bruttoFormatiert
|
||||||
|
*/
|
||||||
|
|
||||||
|
class RechnungsEngine {
|
||||||
|
/**
|
||||||
|
* Konstruktor mit Dependency Injection:
|
||||||
|
* Erlaubt das Austauschen der Module für Tests oder andere Währungen/Formate.
|
||||||
|
* @param {typeof MathModule} [mathMod=MathModule]
|
||||||
|
* @param {typeof FormatterModule} [formatMod=FormatterModule]
|
||||||
|
*/
|
||||||
|
constructor(mathMod = MathModule, formatMod = FormatterModule) {
|
||||||
|
this.math = mathMod;
|
||||||
|
this.formatter = formatMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 3 (TODO 3): erstellePosition(bezeichnung, menge, einzelpreis)
|
||||||
|
* Erstellt eine validierte Rechnungsposition und berechnet den gesamtpreis.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - bezeichnung: string (nach .trim() nicht leer, sonst Error werfen)
|
||||||
|
* - menge: number (> 0, sonst RangeError)
|
||||||
|
* - einzelpreis: number (>= 0, sonst RangeError)
|
||||||
|
* - gesamtpreis berechnen über this.math.multiplizieren(menge, einzelpreis)
|
||||||
|
* - Rückgabe: { bezeichnung, menge, einzelpreis, gesamtpreis }
|
||||||
|
*
|
||||||
|
* @param {string} bezeichnung
|
||||||
|
* @param {number} menge
|
||||||
|
* @param {number} einzelpreis
|
||||||
|
* @returns {Rechnungsposition}
|
||||||
|
*/
|
||||||
|
erstellePosition(bezeichnung, menge, einzelpreis) {
|
||||||
|
// TODO: Validierung & Erstellung der Position
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 4 (TODO 4): berechneRechnung(positionen, steuersatzProzent = 19)
|
||||||
|
* Berechnet Gesamtsummen, Steuern und formatierte Ausgaben für eine Liste von Positionen.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - positionen: Array von Rechnungspositionen (falls leer, Summen = 0)
|
||||||
|
* - steuersatzProzent: number (Standard: 19)
|
||||||
|
* - nettoSumme: Summe aller p.gesamtpreis (mit this.math.addieren berechnen)
|
||||||
|
* - steuerBetrag: this.math.runden(this.math.multiplizieren(nettoSumme, steuersatzProzent / 100), 2)
|
||||||
|
* - bruttoSumme: this.math.addieren(nettoSumme, steuerBetrag)
|
||||||
|
* - Formatierte Werte über this.formatter.formatiereWaehrung() erzeugen
|
||||||
|
* - Rückgabe: Das fertige Rechnung-Objekt
|
||||||
|
*
|
||||||
|
* @param {Rechnungsposition[]} positionen
|
||||||
|
* @param {number} [steuersatzProzent=19]
|
||||||
|
* @returns {Rechnung}
|
||||||
|
*/
|
||||||
|
berechneRechnung(positionen, steuersatzProzent = 19) {
|
||||||
|
// TODO: Netto, Steuer, Brutto und formatierte Werte berechnen
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
151
courses/javascript/13_es_module_und_architektur/index.html
Normal file
151
courses/javascript/13_es_module_und_architektur/index.html
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>JS 13: ES Module & Architektur</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🧱 Software-Architektur & ES6 Module</div>
|
||||||
|
<h1>JS 13: ES Module & Architektur</h1>
|
||||||
|
<p>Lerne saubere Code-Strukturen mit ES6 Import/Export, Separation of Concerns und Dependency Injection für skalierbare Web-Anwendungen.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 📂 Schnellzugriff & Download -->
|
||||||
|
<div style="background: #ffffff; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; box-shadow: var(--shadow-sm);">
|
||||||
|
<div style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>📂 Dateien dieser Einheit:</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<a href="aufgabe.js" class="btn" style="padding: 6px 14px; font-size: 0.88rem; background: var(--primary);" download>📄 aufgabe.js (Startercode)</a>
|
||||||
|
<a href="test_aufgabe.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🧪 test_aufgabe.js (Tests)</a>
|
||||||
|
<a href="musterloesung.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>💡 musterloesung.js (Lösung)</a>
|
||||||
|
<a href="README.md" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;">📖 README.md</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. THEORIE: ES MODULES -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. 📦 ES6 Modules: Exportieren & Importieren</h2>
|
||||||
|
<p>In modernem JavaScript teilst du komplexen Code in wiederverwendbare, isolierte Module auf:</p>
|
||||||
|
|
||||||
|
<pre><code>// 📁 utils/math.js (Named Exports)
|
||||||
|
export const PI = 3.14159;
|
||||||
|
export function addieren(a, b) { return a + b; }
|
||||||
|
|
||||||
|
// 📁 models/Rechner.js (Default Export)
|
||||||
|
export default class Rechner { ... }
|
||||||
|
|
||||||
|
// 📁 app.js (Gezielter & Default Import)
|
||||||
|
import Rechner from "./models/Rechner.js";
|
||||||
|
import { addieren, PI } from "./utils/math.js";
|
||||||
|
import * as Formatters from "./utils/format.js";</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🧱 Didaktische Analogie: Modulare Legosteine & Container</div>
|
||||||
|
<p>Monolithischer Code gleicht einer in Stein gemeißelten Statue: Ändert man einen Arm, bricht die gesamte Skulptur. Modulare Software ist wie ein <strong>Container-Hafen</strong>: Jeder Container hat genormte Maße (Exports/Imports), arbeitet innen autark und kann beliebig ausgetauscht oder transportiert werden.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: SEPARATION OF CONCERNS -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. 🏗️ Separation of Concerns (SoC)</h2>
|
||||||
|
<p>Jedes Modul hat genau <strong>eine</strong> Verantwortung. Das verhindert Nebeneffekte und macht Unittests zum Kinderspiel:</p>
|
||||||
|
|
||||||
|
<div style="background: #0f172a; border: 1px solid #334155; border-radius: var(--radius-sm); padding: 16px; margin: 15px 0; color: #f8fafc; font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.6;">
|
||||||
|
<div style="color: #38bdf8; font-weight: 700; margin-bottom: 8px;">🏛️ Schichten-Architektur: Rechnungs-System</div>
|
||||||
|
<div style="border-left: 2px solid #38bdf8; padding-left: 12px; margin-bottom: 10px;">
|
||||||
|
<strong>1. Controller / Orchestrator (<code>RechnungsEngine</code>):</strong> Steuert den Ablauf, kennt aber keine Formatierungsdetails.
|
||||||
|
</div>
|
||||||
|
<div style="border-left: 2px solid #34d399; padding-left: 12px; margin-bottom: 10px;">
|
||||||
|
<strong>2. Business Logic (<code>MathModule</code>):</strong> Rechnet mathematisch exakt, wirft Fehler bei 0-Division.
|
||||||
|
</div>
|
||||||
|
<div style="border-left: 2px solid #fbbf24; padding-left: 12px;">
|
||||||
|
<strong>3. Presentation Layer (<code>FormatterModule</code>):</strong> Bereitet Zahlen als <code>"1.250,50 EUR"</code> und Datumsangaben für Menschen auf.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. CHEAT-SHEET -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 3. Cheat-Sheet: Modul-Syntax & Konzepte</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Konstrukt</th>
|
||||||
|
<th style="padding: 10px;">Syntax-Beispiel</th>
|
||||||
|
<th style="padding: 10px;">Bedeutung</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Named Export</code></td>
|
||||||
|
<td style="padding: 10px;"><code>export function add() {}</code></td>
|
||||||
|
<td style="padding: 10px;">Exportiert mehrere benannte Elemente</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Default Export</code></td>
|
||||||
|
<td style="padding: 10px;"><code>export default class App {}</code></td>
|
||||||
|
<td style="padding: 10px;">Hauptexport eines Moduls (1x pro Datei)</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Named Import</code></td>
|
||||||
|
<td style="padding: 10px;"><code>import { add } from './math.js'</code></td>
|
||||||
|
<td style="padding: 10px;">Gezieltes Importieren einzelner Funktionen</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Namespace Import</code></td>
|
||||||
|
<td style="padding: 10px;"><code>import * as M from './math.js'</code></td>
|
||||||
|
<td style="padding: 10px;">Bündelt alle Exporte als Objekt</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>Dependency Injection</code></td>
|
||||||
|
<td style="padding: 10px;"><code>new Engine(mathMod, fmtMod)</code></td>
|
||||||
|
<td style="padding: 10px;">Abhängigkeiten von außen übergeben (Testbarkeit)</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 4. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>MathModule</code> – Grundrechenarten (<code>addieren</code>, <code>subtrahieren</code>, <code>multiplizieren</code>, <code>dividieren</code>) mit Schutz vor 0-Division und <code>runden</code> umsetzen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>FormatterModule</code> – Deutsches Währungsformat (<code>formatiereWaehrung</code>) und ISO-Datum (<code>formatiereDatumIso</code>) implementieren.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>RechnungsEngine.erstellePosition</code> – Validierung & Gesamtpreis über <code>this.math</code> berechnen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>RechnungsEngine.berechneRechnung</code> – Netto, Steuer & Brutto berechnen und Ausgaben mit <code>this.formatter</code> formatieren.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_4_architektur_und_master&chapter=13_es_module_und_architektur" class="btn" style="background: #eab308; color: #000; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../12_async_await_und_fetch/index.html" class="btn btn-secondary">← Zurück zu JS 12 (Async & Fetch)</a>
|
||||||
|
<a href="../15_typescript_fortgeschritten/index.html" class="btn btn-secondary">Weiter zu TS 15 (TypeScript Fortgeschritten) →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../../assets/ui_enhancements.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
139
courses/javascript/13_es_module_und_architektur/musterloesung.js
Normal file
139
courses/javascript/13_es_module_und_architektur/musterloesung.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: JS 13 – ES MODULE & ARCHITEKTUR 🌟
|
||||||
|
* ===================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
function pruefeZahl(val, name = "Wert") {
|
||||||
|
if (typeof val !== "number" || isNaN(val)) {
|
||||||
|
throw new TypeError(`${name} muss eine gültige Zahl sein`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const MathModule = {
|
||||||
|
addieren(a, b) {
|
||||||
|
pruefeZahl(a, "Parameter a");
|
||||||
|
pruefeZahl(b, "Parameter b");
|
||||||
|
return Math.round((a + b) * 1e8) / 1e8;
|
||||||
|
},
|
||||||
|
|
||||||
|
subtrahieren(a, b) {
|
||||||
|
pruefeZahl(a, "Parameter a");
|
||||||
|
pruefeZahl(b, "Parameter b");
|
||||||
|
return Math.round((a - b) * 1e8) / 1e8;
|
||||||
|
},
|
||||||
|
|
||||||
|
multiplizieren(a, b) {
|
||||||
|
pruefeZahl(a, "Parameter a");
|
||||||
|
pruefeZahl(b, "Parameter b");
|
||||||
|
return Math.round((a * b) * 1e8) / 1e8;
|
||||||
|
},
|
||||||
|
|
||||||
|
dividieren(a, b) {
|
||||||
|
pruefeZahl(a, "Parameter a");
|
||||||
|
pruefeZahl(b, "Parameter b");
|
||||||
|
if (b === 0) {
|
||||||
|
throw new Error("Division durch 0 nicht erlaubt");
|
||||||
|
}
|
||||||
|
return Math.round((a / b) * 1e8) / 1e8;
|
||||||
|
},
|
||||||
|
|
||||||
|
runden(betrag, dezimalstellen = 2) {
|
||||||
|
pruefeZahl(betrag, "Betrag");
|
||||||
|
pruefeZahl(dezimalstellen, "Dezimalstellen");
|
||||||
|
const faktor = Math.pow(10, dezimalstellen);
|
||||||
|
return Math.round((betrag + Number.EPSILON) * faktor) / faktor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormatterModule = {
|
||||||
|
formatiereWaehrung(betrag, waehrung = "EUR") {
|
||||||
|
pruefeZahl(betrag, "Betrag");
|
||||||
|
if (typeof waehrung !== "string" || waehrung.trim().length === 0) {
|
||||||
|
throw new TypeError("Währung muss ein nicht-leerer String sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gerundet = MathModule.runden(betrag, 2);
|
||||||
|
const teile = (Math.abs(gerundet)).toFixed(2).split(".");
|
||||||
|
const vz = gerundet < 0 ? "-" : "";
|
||||||
|
const ganzzahlFormatiert = teile[0].replace(/\B(?=(\d{3})+(?!\d))/g, ".");
|
||||||
|
return `${vz}${ganzzahlFormatiert},${teile[1]} ${waehrung.trim().toUpperCase()}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatiereDatumIso(datum) {
|
||||||
|
let d;
|
||||||
|
if (datum instanceof Date) {
|
||||||
|
d = datum;
|
||||||
|
} else if (typeof datum === "string" || typeof datum === "number") {
|
||||||
|
d = new Date(datum);
|
||||||
|
} else {
|
||||||
|
throw new TypeError("Ungültiges Datumsformat");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(d.getTime())) {
|
||||||
|
throw new TypeError("Ungültiges Datum");
|
||||||
|
}
|
||||||
|
|
||||||
|
const jahr = d.getUTCFullYear();
|
||||||
|
const monat = String(d.getUTCMonth() + 1).padStart(2, "0");
|
||||||
|
const tag = String(d.getUTCDate()).padStart(2, "0");
|
||||||
|
return `${jahr}-${monat}-${tag}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class RechnungsEngine {
|
||||||
|
constructor(mathMod = MathModule, formatMod = FormatterModule) {
|
||||||
|
this.math = mathMod;
|
||||||
|
this.formatter = formatMod;
|
||||||
|
}
|
||||||
|
|
||||||
|
erstellePosition(bezeichnung, menge, einzelpreis) {
|
||||||
|
if (typeof bezeichnung !== "string" || bezeichnung.trim().length === 0) {
|
||||||
|
throw new Error("Bezeichnung darf nicht leer sein");
|
||||||
|
}
|
||||||
|
if (typeof menge !== "number" || isNaN(menge) || menge <= 0) {
|
||||||
|
throw new RangeError("Menge muss eine Zahl größer als 0 sein");
|
||||||
|
}
|
||||||
|
if (typeof einzelpreis !== "number" || isNaN(einzelpreis) || einzelpreis < 0) {
|
||||||
|
throw new RangeError("Einzelpreis muss eine Zahl >= 0 sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
const gesamtpreis = this.math.multiplizieren(menge, einzelpreis);
|
||||||
|
return {
|
||||||
|
bezeichnung: bezeichnung.trim(),
|
||||||
|
menge,
|
||||||
|
einzelpreis,
|
||||||
|
gesamtpreis
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
berechneRechnung(positionen, steuersatzProzent = 19) {
|
||||||
|
if (!Array.isArray(positionen)) {
|
||||||
|
throw new TypeError("Positionen müssen als Array übergeben werden");
|
||||||
|
}
|
||||||
|
if (typeof steuersatzProzent !== "number" || isNaN(steuersatzProzent) || steuersatzProzent < 0) {
|
||||||
|
throw new RangeError("Steuersatz muss eine Zahl >= 0 sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
let nettoSumme = 0;
|
||||||
|
for (const p of positionen) {
|
||||||
|
const gp = (p && typeof p.gesamtpreis === "number") ? p.gesamtpreis : 0;
|
||||||
|
nettoSumme = this.math.addieren(nettoSumme, gp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const steuerFaktor = this.math.dividieren(steuersatzProzent, 100);
|
||||||
|
const steuerUnrund = this.math.multiplizieren(nettoSumme, steuerFaktor);
|
||||||
|
const steuerBetrag = this.math.runden(steuerUnrund, 2);
|
||||||
|
const bruttoSumme = this.math.addieren(nettoSumme, steuerBetrag);
|
||||||
|
|
||||||
|
return {
|
||||||
|
positionen: [...positionen],
|
||||||
|
nettoSumme,
|
||||||
|
steuersatzProzent,
|
||||||
|
steuerBetrag,
|
||||||
|
bruttoSumme,
|
||||||
|
nettoFormatiert: this.formatter.formatiereWaehrung(nettoSumme),
|
||||||
|
steuerFormatiert: this.formatter.formatiereWaehrung(steuerBetrag),
|
||||||
|
bruttoFormatiert: this.formatter.formatiereWaehrung(bruttoSumme)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
111
courses/javascript/13_es_module_und_architektur/test_aufgabe.js
Normal file
111
courses/javascript/13_es_module_und_architektur/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
/**
|
||||||
|
* 🧪 Testsuite für JS 13: ES Module & Architektur
|
||||||
|
*/
|
||||||
|
|
||||||
|
function assertThrows(fn, msg) {
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
assert.ok(threw, msg || "Erwartete Exception wurde nicht geworfen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 1. Tests für MathModule
|
||||||
|
// ----------------------------------------------------
|
||||||
|
assert.strictEqual(MathModule.addieren(12.5, 7.5), 20, "MathModule.addieren(12.5, 7.5) sollte 20 sein");
|
||||||
|
assert.strictEqual(MathModule.subtrahieren(100, 35), 65, "MathModule.subtrahieren(100, 35) sollte 65 sein");
|
||||||
|
assert.strictEqual(MathModule.multiplizieren(6, 7), 42, "MathModule.multiplizieren(6, 7) sollte 42 sein");
|
||||||
|
assert.strictEqual(MathModule.dividieren(100, 4), 25, "MathModule.dividieren(100, 4) sollte 25 sein");
|
||||||
|
assert.strictEqual(MathModule.runden(49.994, 2), 49.99, "MathModule.runden(49.994, 2)");
|
||||||
|
assert.strictEqual(MathModule.runden(49.996, 2), 50.00, "MathModule.runden(49.996, 2)");
|
||||||
|
|
||||||
|
// Validierung & Schutz vor Division durch 0
|
||||||
|
assertThrows(() => MathModule.dividieren(10, 0), "Division durch 0 muss Error werfen");
|
||||||
|
assertThrows(() => MathModule.addieren("10", 5), "String statt Zahl muss TypeError werfen");
|
||||||
|
assertThrows(() => MathModule.multiplizieren(5, NaN), "NaN muss TypeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 2. Tests für FormatterModule
|
||||||
|
// ----------------------------------------------------
|
||||||
|
assert.strictEqual(
|
||||||
|
FormatterModule.formatiereWaehrung(1250.5),
|
||||||
|
"1.250,50 EUR",
|
||||||
|
"formatiereWaehrung(1250.5) mit Standard EUR und Tausender-Punkt"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
FormatterModule.formatiereWaehrung(49.99, "USD"),
|
||||||
|
"49,99 USD",
|
||||||
|
"formatiereWaehrung(49.99, 'USD')"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
FormatterModule.formatiereWaehrung(0, "chf"),
|
||||||
|
"0,00 CHF",
|
||||||
|
"formatiereWaehrung(0, 'chf') sollte Währung großschreiben"
|
||||||
|
);
|
||||||
|
|
||||||
|
const testDatum = new Date(Date.UTC(2026, 7, 30));
|
||||||
|
assert.strictEqual(
|
||||||
|
FormatterModule.formatiereDatumIso(testDatum),
|
||||||
|
"2026-08-30",
|
||||||
|
"formatiereDatumIso(testDatum) sollte '2026-08-30' liefern"
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThrows(() => FormatterModule.formatiereWaehrung("ungültig"), "Ungültiger Betrag muss TypeError werfen");
|
||||||
|
assertThrows(() => FormatterModule.formatiereDatumIso("kein-datum"), "Ungültiges Datum muss TypeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 3. Tests für RechnungsEngine (Positionen)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const engine = new RechnungsEngine();
|
||||||
|
|
||||||
|
const pos1 = engine.erstellePosition("Webdesign & UI", 10, 95);
|
||||||
|
assert.strictEqual(pos1.bezeichnung, "Webdesign & UI");
|
||||||
|
assert.strictEqual(pos1.menge, 10);
|
||||||
|
assert.strictEqual(pos1.einzelpreis, 95);
|
||||||
|
assert.strictEqual(pos1.gesamtpreis, 950, "gesamtpreis sollte 10 * 95 = 950 sein");
|
||||||
|
|
||||||
|
const pos2 = engine.erstellePosition(" Hosting Setup ", 2, 25);
|
||||||
|
assert.strictEqual(pos2.bezeichnung, "Hosting Setup", "Bezeichnung muss mit .trim() bereinigt werden");
|
||||||
|
assert.strictEqual(pos2.gesamtpreis, 50);
|
||||||
|
|
||||||
|
assertThrows(() => engine.erstellePosition("", 1, 50), "Leere Bezeichnung muss Error werfen");
|
||||||
|
assertThrows(() => engine.erstellePosition("Server", 0, 50), "Menge <= 0 muss RangeError werfen");
|
||||||
|
assertThrows(() => engine.erstellePosition("Server", 2, -10), "Negativer Preis muss RangeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 4. Tests für RechnungsEngine.berechneRechnung
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const rechnung = engine.berechneRechnung([pos1, pos2], 19);
|
||||||
|
|
||||||
|
assert.strictEqual(rechnung.positionen.length, 2);
|
||||||
|
assert.strictEqual(rechnung.nettoSumme, 1000, "Netto-Summe 950 + 50 = 1000");
|
||||||
|
assert.strictEqual(rechnung.steuersatzProzent, 19);
|
||||||
|
assert.strictEqual(rechnung.steuerBetrag, 190, "19% Steuer auf 1000 = 190");
|
||||||
|
assert.strictEqual(rechnung.bruttoSumme, 1190, "Brutto-Summe 1000 + 190 = 1190");
|
||||||
|
assert.strictEqual(rechnung.nettoFormatiert, "1.000,00 EUR");
|
||||||
|
assert.strictEqual(rechnung.steuerFormatiert, "190,00 EUR");
|
||||||
|
assert.strictEqual(rechnung.bruttoFormatiert, "1.190,00 EUR");
|
||||||
|
|
||||||
|
// Test Leere Rechnung
|
||||||
|
const leereRechnung = engine.berechneRechnung([]);
|
||||||
|
assert.strictEqual(leereRechnung.nettoSumme, 0);
|
||||||
|
assert.strictEqual(leereRechnung.bruttoSumme, 0);
|
||||||
|
assert.strictEqual(leereRechnung.nettoFormatiert, "0,00 EUR");
|
||||||
|
|
||||||
|
// Test Dependency Injection mit Custom Mock
|
||||||
|
let mockCalcUsed = false;
|
||||||
|
const customMathMock = {
|
||||||
|
...MathModule,
|
||||||
|
multiplizieren(a, b) {
|
||||||
|
mockCalcUsed = true;
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const customEngine = new RechnungsEngine(customMathMock, FormatterModule);
|
||||||
|
customEngine.erstellePosition("Test Mock", 5, 20);
|
||||||
|
assert.strictEqual(mockCalcUsed, true, "Dependency Injection sollte das übergebene Math-Modul nutzen");
|
||||||
|
|
||||||
|
console.log("✅ Alle 20 Tests für JS 13 (ES Module & Architektur) erfolgreich bestanden!");
|
||||||
|
|
@ -163,8 +163,8 @@ const strings = umkehren(["A", "B", "C"]); // Typ bleibt string[]</code></pre>
|
||||||
|
|
||||||
<!-- Footer Navigation -->
|
<!-- Footer Navigation -->
|
||||||
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
|
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
|
||||||
<a href="../02_funktionen_und_arrays/index.html" class="btn btn-secondary">← Zurück zu Modul 02</a>
|
<a href="../13_es_module_und_architektur/index.html" class="btn btn-secondary">← Zurück zu Modul 02</a>
|
||||||
<a href="../08_abschlussprojekt_web_app/index.html" class="btn" style="background: var(--primary);">Weiter zum Master-Projekt Modul 08 →</a>
|
<a href="../15_typescript_fortgeschritten/index.html" class="btn" style="background: var(--primary);">Weiter zum Master-Projekt Modul 08 →</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
116
courses/javascript/15_typescript_fortgeschritten/README.md
Normal file
116
courses/javascript/15_typescript_fortgeschritten/README.md
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
# TS 15: Fortgeschrittenes TypeScript & Generics 🛡️
|
||||||
|
|
||||||
|
Willkommen zu **Modul 15** des JavaScript- & TypeScript-Kurses!
|
||||||
|
|
||||||
|
Einfache Typen wie `string` oder `number` reichen für elementare Skripte aus. In professionellen Enterprise-Anwendungen benötigst du jedoch **generische Container**, **Typ-Wächter (Type Guards)**, **Enums** und **Utility Types**, um hochflexible und dennoch zu 100 % typsichere Architekturen zu bauen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 1. Das Wichtigste in Kürze
|
||||||
|
|
||||||
|
### A. Generics (`<T>`): Wiederverwendbare Typen
|
||||||
|
Statt Code für jeden Datentyp zu duplizieren oder `any` zu verwenden, nutzt du **Generics** als Typ-Platzhalter:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Generisches Interface für paginierte REST-API Antworten:
|
||||||
|
interface PaginiertesErgebnis<T> {
|
||||||
|
items: T[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
totalPages: number;
|
||||||
|
hasNextPage: boolean;
|
||||||
|
hasPrevPage: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verwendung:
|
||||||
|
const benutzerSeite: PaginiertesErgebnis<Benutzer> = erstellePaginiertesErgebnis(users, 100, 1, 10);
|
||||||
|
const produktSeite: PaginiertesErgebnis<Produkt> = erstellePaginiertesErgebnis(products, 50, 2, 20);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### B. Custom Type Guards (`obj is Type`)
|
||||||
|
Mit einem Type Guard teilst du dem TypeScript-Compiler mit, dass ein Objekt nach einer erfolgreichen Laufzeitprüfung einem bestimmten Typ entspricht:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
function istAdminBenutzer(obj: any): obj is AdminBenutzer {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === "object" &&
|
||||||
|
typeof obj.username === "string" &&
|
||||||
|
obj.rolle === "ADMIN" &&
|
||||||
|
Array.isArray(obj.berechtigungen) &&
|
||||||
|
obj.berechtigungen.length > 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// In der Anwendung (TypeScript narrowed den Typ automatisch):
|
||||||
|
if (istAdminBenutzer(aktuellerUser)) {
|
||||||
|
console.log(aktuellerUser.berechtigungen.join(", ")); // ✅ Sicher, kein any!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### C. Enums: Typisierte Konstanten
|
||||||
|
Enums bündeln eine feste Menge zusammengehöriger Werte und verhindern Tippfehler:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
enum BenutzerRolle {
|
||||||
|
ADMIN = "ADMIN",
|
||||||
|
USER = "USER",
|
||||||
|
GAST = "GAST"
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TaskStatus {
|
||||||
|
TODO = "TODO",
|
||||||
|
IN_PROGRESS = "IN_PROGRESS",
|
||||||
|
DONE = "DONE"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### D. TypeScript Utility Types
|
||||||
|
TypeScript bietet eingebaute Werkzeuge zur Transformation bestehender Typen:
|
||||||
|
|
||||||
|
| Utility Type | Syntax | Bedeutung |
|
||||||
|
| :--- | :--- | :--- |
|
||||||
|
| **`Partial<T>`** | `Partial<Benutzer>` | Macht alle Eigenschaften von `T` optional (ideal für Updates/PATCH-Requests). |
|
||||||
|
| **`Readonly<T>`** | `Readonly<Konfiguration>` | Verhindert nachträgliche Veränderungen von Eigenschaften. |
|
||||||
|
| **`Pick<T, K>`** | `Pick<Benutzer, "id" \| "email">` | Wählt eine Teilmenge bestimmter Eigenschaften aus `T` aus. |
|
||||||
|
| **`Omit<T, K>`** | `Omit<Benutzer, "passwortHash">` | Entfernt sensible Eigenschaften aus `T`. |
|
||||||
|
| **`Record<K, T>`** | `Record<string, number>` | Erstellt ein Dictionary/Map mit Schlüsseln vom Typ `K` und Werten vom Typ `T`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🩻 Die didaktische Analogie: "Der Hightech-Röntgenapparat & der Präzisions-Maßanzug"
|
||||||
|
|
||||||
|
- **`any` ist die Augenbinde:**
|
||||||
|
Man ignoriert alle Gefahren und hofft, dass auf der Baustelle nichts schiefgeht.
|
||||||
|
- **Generics (`<T>`) sind wie maßgeschneiderte Transportbehälter:**
|
||||||
|
Ein Behälter mit Schutzpolsterung für Gläser transportiert Gläser bruchsicher. Dasselbe Design transportiert Goldbarren. Der Inhalt bleibt exakt typisiert, ohne dass der Behälter den Inhalt beschädigt.
|
||||||
|
- **Type Guards sind der Hightech-Röntgenapparat am Flughafen:**
|
||||||
|
Jedes Gepäckstück (unbekannte Daten) wird an der Schleuse durchleuchtet. Nur wenn alle Kriterien (ID, Admin-Rolle, Berechtigungen) einwandfrei erkannt werden, öffnet die Schranke in den VIP-Bereich.
|
||||||
|
- **Utility Types (`Partial`, `Pick`) sind die Schneiderwerkzeuge:**
|
||||||
|
Du hast einen kompletten Maßanzug (Großes Interface) und schneidest daraus für den Sommer präzise eine Weste (`Pick`) oder machst die Knöpfe flexibel verstellbar (`Partial`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎯 Aufgaben & Teilziele in `aufgabe.js`
|
||||||
|
|
||||||
|
1. **TODO 1:** `erstellePaginiertesErgebnis(items, total, page = 1, pageSize = 10)`
|
||||||
|
Erstelle einen generischen Container `<T>` mit Berechnung von `totalPages`, `hasNextPage` und `hasPrevPage`.
|
||||||
|
2. **TODO 2:** `istAdminBenutzer(benutzerObjekt)`
|
||||||
|
Implementiere einen Custom Type Guard, der ID, Username, Email, Admin-Rolle und Berechtigungs-Array prüft.
|
||||||
|
3. **TODO 3:** `aktualisiereBenutzerProfil(original, partiellesUpdate)`
|
||||||
|
Führe ein partielles Update (`Partial<T>`) mit Validierung durch, ohne das Original zu mutieren (Immutability).
|
||||||
|
4. **TODO 4:** `filtriereFelder(objekt, erlaubteSchluessel)`
|
||||||
|
Filtere die Eigenschaften eines Objekts (`Pick<T, K>`) und ignoriere nicht vorhandene Schlüssel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Tests ausführen
|
||||||
|
|
||||||
|
Öffne die Web-IDE oder führe die Unittests aus. Sobald alle 22 Tests grün sind, beherrschst du TypeScript auf Profi-Niveau!
|
||||||
145
courses/javascript/15_typescript_fortgeschritten/aufgabe.js
Normal file
145
courses/javascript/15_typescript_fortgeschritten/aufgabe.js
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
/**
|
||||||
|
* 🌐 TS 15: FORTGESCHRITTENES TYPESCRIPT & GENERICS 🌐
|
||||||
|
* ====================================================
|
||||||
|
* In diesem Modul lernst du Profi-Konzepte:
|
||||||
|
* - Generics (<T>) für wiederverwendbare Datencontainer
|
||||||
|
* - Custom Type Guards (is Type) für präzise Typ-Eingrenzung
|
||||||
|
* - Enums für typisierte Konstanten
|
||||||
|
* - Utility Types (Partial<T>, Pick<T, K>, Readonly<T>)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 ENUM: BenutzerRolle
|
||||||
|
* Definiert alle gültigen Rollen im System.
|
||||||
|
*/
|
||||||
|
const BenutzerRolle = {
|
||||||
|
ADMIN: "ADMIN",
|
||||||
|
USER: "USER",
|
||||||
|
GAST: "GAST"
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @template T
|
||||||
|
* @typedef {Object} PaginiertesErgebnis
|
||||||
|
* @property {T[]} items - Die Datensätze der aktuellen Seite
|
||||||
|
* @property {number} total - Gesamtzahl aller Datensätze
|
||||||
|
* @property {number} page - Aktuelle Seitennummer (1-basiert)
|
||||||
|
* @property {number} pageSize - Anzahl Elemente pro Seite
|
||||||
|
* @property {number} totalPages - Berechnete Gesamtseitenzahl
|
||||||
|
* @property {boolean} hasNextPage - Ob eine Folgeseite existiert
|
||||||
|
* @property {boolean} hasPrevPage - Ob eine vorherige Seite existiert
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 1 (TODO 1): erstellePaginiertesErgebnis(items, total, page, pageSize)
|
||||||
|
* Generische Hilfsfunktion für paginierte REST-API Antworten (Generic Container <T>).
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - items: T[] (muss ein Array sein, sonst TypeError)
|
||||||
|
* - total: number (Zahl >= 0, sonst RangeError)
|
||||||
|
* - page: number (Zahl >= 1, Standard: 1, sonst RangeError)
|
||||||
|
* - pageSize: number (Zahl >= 1, Standard: 10, sonst RangeError)
|
||||||
|
* - totalPages berechnen: Math.max(1, Math.ceil(total / pageSize))
|
||||||
|
* - hasNextPage: page < totalPages
|
||||||
|
* - hasPrevPage: page > 1
|
||||||
|
* - Rückgabe: Das fertige PaginiertesErgebnis-Objekt
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {T[]} items
|
||||||
|
* @param {number} total
|
||||||
|
* @param {number} [page=1]
|
||||||
|
* @param {number} [pageSize=10]
|
||||||
|
* @returns {PaginiertesErgebnis<T>}
|
||||||
|
*/
|
||||||
|
function erstellePaginiertesErgebnis(items, total, page = 1, pageSize = 10) {
|
||||||
|
// TODO: Parameter validieren, Metadaten berechnen und Objekt zurückgeben
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} AdminBenutzer
|
||||||
|
* @property {string | number} id - Eindeutige ID
|
||||||
|
* @property {string} username - Benutzername
|
||||||
|
* @property {string} email - Gültige E-Mail-Adresse
|
||||||
|
* @property {'ADMIN'} rolle - Rolle muss ADMIN sein
|
||||||
|
* @property {string[]} berechtigungen - Array mit mindestens einer Berechtigung
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 2 (TODO 2): istAdminBenutzer(benutzerObjekt)
|
||||||
|
* Benutzerdefinierter Type Guard zur Laufzeitprüfung eines Objekts auf Admin-Konformität.
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - Prüft, ob benutzerObjekt ein echtes Objekt ist (nicht null / undefined).
|
||||||
|
* - id: Vorhanden (string oder number).
|
||||||
|
* - username: string (nach .trim() nicht leer).
|
||||||
|
* - email: string (muss "@" enthalten).
|
||||||
|
* - rolle: exakt "ADMIN" (oder BenutzerRolle.ADMIN).
|
||||||
|
* - berechtigungen: Array mit mindestens 1 Element (length > 0).
|
||||||
|
* - Gibt true zurück wenn ALLE Kriterien erfüllt sind, sonst false (wirft KEINEN Fehler).
|
||||||
|
*
|
||||||
|
* @param {any} benutzerObjekt
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function istAdminBenutzer(benutzerObjekt) {
|
||||||
|
// TODO: Alle Eigenschaften des Admin-Interfaces auf Gültigkeit prüfen
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} BenutzerProfil
|
||||||
|
* @property {number} id
|
||||||
|
* @property {string} username
|
||||||
|
* @property {string} email
|
||||||
|
* @property {Object} einstellungen
|
||||||
|
* @property {boolean} einstellungen.darkMode
|
||||||
|
* @property {boolean} einstellungen.benachrichtigungen
|
||||||
|
* @property {string} [aktualisiertAm]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 3 (TODO 3): aktualisiereBenutzerProfil(original, partiellesUpdate)
|
||||||
|
* Führt ein partielles Update eines Profils durch (Semantik von TypeScript Partial<T>).
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - original: BenutzerProfil (muss ein gültiges Objekt sein, sonst TypeError)
|
||||||
|
* - partiellesUpdate: Partial<BenutzerProfil> (optionale Teilmenge aller Felder)
|
||||||
|
* - Validierung: Falls im Update Felder übergeben wurden, müssen die Typen stimmen:
|
||||||
|
* - email (falls vorhanden): muss string sein und "@" enthalten, sonst TypeError.
|
||||||
|
* - username (falls vorhanden): muss string sein und nicht leer, sonst TypeError.
|
||||||
|
* - Immutability: Das `original`-Objekt darf NICHT verändert werden!
|
||||||
|
* - Verschmilzt `original` mit `partiellesUpdate` (inklusive geschachtelter `einstellungen`).
|
||||||
|
* - Setzt das Feld `aktualisiertAm` auf den aktuellen ISO-Datumsstring.
|
||||||
|
* - Rückgabe: Das neue, aktualisierte BenutzerProfil-Objekt.
|
||||||
|
*
|
||||||
|
* @param {BenutzerProfil} original
|
||||||
|
* @param {Partial<BenutzerProfil>} partiellesUpdate
|
||||||
|
* @returns {BenutzerProfil}
|
||||||
|
*/
|
||||||
|
function aktualisiereBenutzerProfil(original, partiellesUpdate = {}) {
|
||||||
|
// TODO: Validierung, Immutable Merge und aktualisiertAm setzen
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🎯 TEILZIEL 4 (TODO 4): filtriereFelder(objekt, erlaubteSchluessel)
|
||||||
|
* Erstellt ein neues Objekt mit einer Teilmenge der Eigenschaften (Semantik von Pick<T, K>).
|
||||||
|
*
|
||||||
|
* Anforderungen:
|
||||||
|
* - objekt: Beliebiges Objekt (falls kein Objekt oder null, leeres Objekt {} zurückgeben)
|
||||||
|
* - erlaubteSchluessel: Array von Strings (z.B. ["id", "username"])
|
||||||
|
* - Erstellt ein neues Objekt, das NUR die Eigenschaften aus `erlaubteSchluessel` enthält,
|
||||||
|
* die auch tatsächlich im Ausgangsobjekt existieren.
|
||||||
|
* - Nicht vorhandene Schlüssel werden ignoriert (kein undefined als Wert einfügen).
|
||||||
|
* - Gibt das neue gefilterte Objekt zurück.
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @template {keyof T} K
|
||||||
|
* @param {T} objekt
|
||||||
|
* @param {K[]} erlaubteSchluessel
|
||||||
|
* @returns {Pick<T, K>}
|
||||||
|
*/
|
||||||
|
function filtriereFelder(objekt, erlaubteSchluessel) {
|
||||||
|
// TODO: Neues Objekt erstellen und nur erlaubte Schlüssel kopieren
|
||||||
|
return {};
|
||||||
|
}
|
||||||
155
courses/javascript/15_typescript_fortgeschritten/index.html
Normal file
155
courses/javascript/15_typescript_fortgeschritten/index.html
Normal file
|
|
@ -0,0 +1,155 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>TS 15: Fortgeschrittenes TypeScript & Generics</title>
|
||||||
|
<link rel="stylesheet" href="../../../assets/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app-layout">
|
||||||
|
<div class="app-main">
|
||||||
|
<header>
|
||||||
|
<div class="header-container">
|
||||||
|
<div class="header-badge">🛡️ TypeScript Advanced & Generics</div>
|
||||||
|
<h1>TS 15: Fortgeschrittenes TypeScript & Generics</h1>
|
||||||
|
<p>Beherrsche fortgeschrittene Typsystem-Konzepte: Generics <code><T></code>, Custom Type Guards, Enums und Utility Types (<code>Partial</code>, <code>Pick</code>, <code>Record</code>).</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<!-- 📂 Schnellzugriff & Download -->
|
||||||
|
<div style="background: #ffffff; border: 1px solid var(--border-color); border-radius: var(--radius-md); padding: 14px 20px; margin-bottom: 25px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px; box-shadow: var(--shadow-sm);">
|
||||||
|
<div style="font-weight: 700; color: var(--text-main); display: flex; align-items: center; gap: 8px;">
|
||||||
|
<span>📂 Dateien dieser Einheit:</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
|
<a href="aufgabe.js" class="btn" style="padding: 6px 14px; font-size: 0.88rem; background: var(--primary);" download>📄 aufgabe.js (Startercode)</a>
|
||||||
|
<a href="test_aufgabe.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>🧪 test_aufgabe.js (Tests)</a>
|
||||||
|
<a href="musterloesung.js" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;" download>💡 musterloesung.js (Lösung)</a>
|
||||||
|
<a href="README.md" class="btn btn-secondary" style="padding: 6px 14px; font-size: 0.88rem;">📖 README.md</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 1. THEORIE: GENERICS -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>1. 🧬 Generics <code><T></code>: Flexible & Typsichere Container</h2>
|
||||||
|
<p>Mit Generics erstellst du Datenstrukturen und Funktionen, die mit beliebigen Typen arbeiten, ohne die Typsicherheit zu verlieren:</p>
|
||||||
|
|
||||||
|
<div style="background: #0f172a; border: 1px solid #334155; border-radius: var(--radius-sm); padding: 16px; margin: 15px 0; color: #f8fafc; font-family: var(--font-mono); font-size: 0.85rem; line-height: 1.6;">
|
||||||
|
<div style="color: #38bdf8; font-weight: 700; margin-bottom: 8px;">📊 Generic Interface: PaginiertesErgebnis<T></div>
|
||||||
|
<div style="border-left: 2px solid #38bdf8; padding-left: 12px;">
|
||||||
|
<span style="color: #fbbf24;">interface PaginiertesErgebnis<T></span> {<br>
|
||||||
|
items: <span style="color: #34d399;">T[]</span>;<br>
|
||||||
|
total: <span style="color: #a78bfa;">number</span>;<br>
|
||||||
|
page: <span style="color: #a78bfa;">number</span>;<br>
|
||||||
|
pageSize: <span style="color: #a78bfa;">number</span>;<br>
|
||||||
|
totalPages: <span style="color: #a78bfa;">number</span>;<br>
|
||||||
|
hasNextPage: <span style="color: #f472b6;">boolean</span>;<br>
|
||||||
|
hasPrevPage: <span style="color: #f472b6;">boolean</span>;<br>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<pre><code>// 🎯 Verwendung mit verschiedenen Datentypen:
|
||||||
|
const userPage: PaginiertesErgebnis<User> = erstellePaginiertesErgebnis(users, 100, 1, 10);
|
||||||
|
const taskPage: PaginiertesErgebnis<Task> = erstellePaginiertesErgebnis(tasks, 45, 2, 10);</code></pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 2. THEORIE: TYPE GUARDS -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>2. 🔍 Type Guards & Type Narrowing</h2>
|
||||||
|
<p>Ein <strong>Custom Type Guard</strong> (<code>obj is TargetType</code>) prüft zur Laufzeit Daten und versichert dem Compiler den genauen Typ:</p>
|
||||||
|
|
||||||
|
<pre><code>function istAdminBenutzer(obj: any): obj is AdminBenutzer {
|
||||||
|
return (
|
||||||
|
obj !== null &&
|
||||||
|
typeof obj === "object" &&
|
||||||
|
obj.rolle === "ADMIN" &&
|
||||||
|
Array.isArray(obj.berechtigungen) &&
|
||||||
|
obj.berechtigungen.length > 0
|
||||||
|
);
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<div class="box box-analogie">
|
||||||
|
<div class="box-title">🩻 Didaktische Analogie: Der Röntgenapparat & der Maßanzug</div>
|
||||||
|
<p><strong>Type Guards</strong> sind wie der <strong>Flughafen-Röntgenapparat</strong>: Unbekannte Daten werden gründlich durchleuchtet, bevor sie in geschützte Bereiche gelassen werden. <strong>Utility Types</strong> sind wie der <strong>Präzisions-Maßanzug</strong>: Aus einem Basis-Schnittmuster schneidest du passgenau eine Weste (<code>Pick</code>) oder ein flexibles Übergangshemd (<code>Partial</code>).</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 3. CHEAT-SHEET: UTILITY TYPES -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>📑 3. Cheat-Sheet: TypeScript Utility Types</h2>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem; text-align: left;">
|
||||||
|
<thead>
|
||||||
|
<tr style="border-bottom: 2px solid var(--border-color); background: var(--bg-subtle);">
|
||||||
|
<th style="padding: 10px;">Utility Type</th>
|
||||||
|
<th style="padding: 10px;">Syntax</th>
|
||||||
|
<th style="padding: 10px;">Wirkung & Einsatzgebiet</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Partial<T></code></td>
|
||||||
|
<td style="padding: 10px;"><code>Partial<UserProfile></code></td>
|
||||||
|
<td style="padding: 10px;">Alle Eigenschaften werden optional (<code>?</code>). Ideal für Updates.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Readonly<T></code></td>
|
||||||
|
<td style="padding: 10px;"><code>Readonly<Config></code></td>
|
||||||
|
<td style="padding: 10px;">Verhindert Überschreiben (Immutability).</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Pick<T, K></code></td>
|
||||||
|
<td style="padding: 10px;"><code>Pick<User, "id" | "email"></code></td>
|
||||||
|
<td style="padding: 10px;">Wählt nur bestimmte Schlüssel aus <code>T</code> aus.</td>
|
||||||
|
</tr>
|
||||||
|
<tr style="border-bottom: 1px solid var(--border-color);">
|
||||||
|
<td style="padding: 10px;"><code>Omit<T, K></code></td>
|
||||||
|
<td style="padding: 10px;"><code>Omit<User, "passwordHash"></code></td>
|
||||||
|
<td style="padding: 10px;">Entfernt bestimmte Schlüssel aus <code>T</code>.</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="padding: 10px;"><code>Record<K, T></code></td>
|
||||||
|
<td style="padding: 10px;"><code>Record<string, number></code></td>
|
||||||
|
<td style="padding: 10px;">Erstellt typisierte Key-Value Maps / Dictionaries.</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- 4. AUFGABEN CHECKLISTE -->
|
||||||
|
<section class="card">
|
||||||
|
<h2>🎯 4. Aufgaben & Checkliste in <code>aufgabe.js</code></h2>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t1">
|
||||||
|
<label class="task-label" for="t1"><strong>TODO 1:</strong> <code>erstellePaginiertesErgebnis(items, total, page, pageSize)</code> – Generischen Container mit Metadaten (<code>totalPages</code>, <code>hasNextPage</code>, <code>hasPrevPage</code>) berechnen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t2">
|
||||||
|
<label class="task-label" for="t2"><strong>TODO 2:</strong> <code>istAdminBenutzer(benutzerObjekt)</code> – Custom Type Guard für ID, Username, Email, Admin-Rolle & Berechtigungen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t3">
|
||||||
|
<label class="task-label" for="t3"><strong>TODO 3:</strong> <code>aktualisiereBenutzerProfil(original, partiellesUpdate)</code> – Partielle Updates (<code>Partial<T></code>) mit Validierung & Immutability ausführen.</label>
|
||||||
|
</div>
|
||||||
|
<div class="task-item">
|
||||||
|
<input type="checkbox" class="task-checkbox" id="t4">
|
||||||
|
<label class="task-label" for="t4"><strong>TODO 4:</strong> <code>filtriereFelder(objekt, erlaubteSchluessel)</code> – Teilmenge von Objekteigenschaften (<code>Pick<T, K></code>) extrahieren.</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 25px; display: flex; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<a href="../../../workspace.html?course=javascript&track=track_4_architektur_und_master&chapter=15_typescript_fortgeschritten" class="btn" style="background: #3178c6; color: #fff; font-weight: 700;">💻 In Web-IDE öffnen →</a>
|
||||||
|
<a href="../13_es_module_und_architektur/index.html" class="btn btn-secondary">← Zurück zu JS 13 (ES Module)</a>
|
||||||
|
<a href="../16_master_abschlussprojekt_web_app/index.html" class="btn btn-secondary">Weiter zum Master-Projekt →</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="../../../assets/ui_enhancements.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
/**
|
||||||
|
* 🌟 MUSTERLÖSUNG: TS 15 – FORTGESCHRITTENES TYPESCRIPT & GENERICS 🌟
|
||||||
|
* ===================================================================
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BenutzerRolle = {
|
||||||
|
ADMIN: "ADMIN",
|
||||||
|
USER: "USER",
|
||||||
|
GAST: "GAST"
|
||||||
|
};
|
||||||
|
|
||||||
|
function erstellePaginiertesErgebnis(items, total, page = 1, pageSize = 10) {
|
||||||
|
if (!Array.isArray(items)) {
|
||||||
|
throw new TypeError("Items müssen als Array übergeben werden");
|
||||||
|
}
|
||||||
|
if (typeof total !== "number" || isNaN(total) || total < 0) {
|
||||||
|
throw new RangeError("Total muss eine Zahl >= 0 sein");
|
||||||
|
}
|
||||||
|
if (typeof page !== "number" || isNaN(page) || page < 1) {
|
||||||
|
throw new RangeError("Page muss eine Zahl >= 1 sein");
|
||||||
|
}
|
||||||
|
if (typeof pageSize !== "number" || isNaN(pageSize) || pageSize < 1) {
|
||||||
|
throw new RangeError("PageSize muss eine Zahl >= 1 sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalPages = total === 0 ? 1 : Math.ceil(total / pageSize);
|
||||||
|
const hasNextPage = page < totalPages;
|
||||||
|
const hasPrevPage = page > 1;
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: [...items],
|
||||||
|
total,
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
totalPages,
|
||||||
|
hasNextPage,
|
||||||
|
hasPrevPage
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function istAdminBenutzer(benutzerObjekt) {
|
||||||
|
if (!benutzerObjekt || typeof benutzerObjekt !== "object" || Array.isArray(benutzerObjekt)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. ID prüfen
|
||||||
|
const hasId = (typeof benutzerObjekt.id === "string" && benutzerObjekt.id.trim().length > 0) ||
|
||||||
|
(typeof benutzerObjekt.id === "number" && !isNaN(benutzerObjekt.id));
|
||||||
|
if (!hasId) return false;
|
||||||
|
|
||||||
|
// 2. Username prüfen
|
||||||
|
if (typeof benutzerObjekt.username !== "string" || benutzerObjekt.username.trim().length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Email prüfen
|
||||||
|
if (typeof benutzerObjekt.email !== "string" || !benutzerObjekt.email.includes("@")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Rolle prüfen (muss ADMIN sein)
|
||||||
|
const istRolleAdmin = benutzerObjekt.rolle === "ADMIN" ||
|
||||||
|
benutzerObjekt.role === "ADMIN" ||
|
||||||
|
benutzerObjekt.rolle === BenutzerRolle.ADMIN;
|
||||||
|
if (!istRolleAdmin) return false;
|
||||||
|
|
||||||
|
// 5. Berechtigungen prüfen (Array mit length > 0)
|
||||||
|
if (!Array.isArray(benutzerObjekt.berechtigungen) || benutzerObjekt.berechtigungen.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function aktualisiereBenutzerProfil(original, partiellesUpdate = {}) {
|
||||||
|
if (!original || typeof original !== "object" || Array.isArray(original)) {
|
||||||
|
throw new TypeError("Original muss ein gültiges Benutzer-Objekt sein");
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = (partiellesUpdate && typeof partiellesUpdate === "object" && !Array.isArray(partiellesUpdate))
|
||||||
|
? partiellesUpdate
|
||||||
|
: {};
|
||||||
|
|
||||||
|
// Validierung der Update-Felder falls übergeben
|
||||||
|
if (update.username !== undefined) {
|
||||||
|
if (typeof update.username !== "string" || update.username.trim().length === 0) {
|
||||||
|
throw new TypeError("Username muss ein nicht-leerer String sein");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (update.email !== undefined) {
|
||||||
|
if (typeof update.email !== "string" || !update.email.includes("@")) {
|
||||||
|
throw new TypeError("E-Mail muss ein gültiger E-Mail-String mit '@' sein");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bestehendeEinstellungen = (original.einstellungen && typeof original.einstellungen === "object")
|
||||||
|
? original.einstellungen
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const neueEinstellungen = (update.einstellungen && typeof update.einstellungen === "object")
|
||||||
|
? update.einstellungen
|
||||||
|
: {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
...update,
|
||||||
|
username: update.username !== undefined ? update.username.trim() : original.username,
|
||||||
|
einstellungen: {
|
||||||
|
...bestehendeEinstellungen,
|
||||||
|
...neueEinstellungen
|
||||||
|
},
|
||||||
|
aktualisiertAm: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function filtriereFelder(objekt, erlaubteSchluessel) {
|
||||||
|
if (!objekt || typeof objekt !== "object" || !Array.isArray(erlaubteSchluessel)) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ergebnis = {};
|
||||||
|
for (const schluessel of erlaubteSchluessel) {
|
||||||
|
if (typeof schluessel === "string" && Object.prototype.hasOwnProperty.call(objekt, schluessel)) {
|
||||||
|
ergebnis[schluessel] = objekt[schluessel];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ergebnis;
|
||||||
|
}
|
||||||
154
courses/javascript/15_typescript_fortgeschritten/test_aufgabe.js
Normal file
154
courses/javascript/15_typescript_fortgeschritten/test_aufgabe.js
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
/**
|
||||||
|
* 🧪 Testsuite für TS 15: Fortgeschrittenes TypeScript & Generics
|
||||||
|
*/
|
||||||
|
|
||||||
|
function assertThrows(fn, msg) {
|
||||||
|
let threw = false;
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} catch (e) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
assert.ok(threw, msg || "Erwartete Exception wurde nicht geworfen!");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 1. Tests für erstellePaginiertesErgebnis (Generics Container)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const datenSeite1 = [
|
||||||
|
{ id: 1, name: "Alpha" },
|
||||||
|
{ id: 2, name: "Beta" },
|
||||||
|
{ id: 3, name: "Gamma" }
|
||||||
|
];
|
||||||
|
|
||||||
|
const res1 = erstellePaginiertesErgebnis(datenSeite1, 25, 1, 10);
|
||||||
|
assert.strictEqual(res1.items.length, 3, "Items-Länge");
|
||||||
|
assert.strictEqual(res1.total, 25, "Gesamtanzahl");
|
||||||
|
assert.strictEqual(res1.page, 1, "Aktuelle Seite");
|
||||||
|
assert.strictEqual(res1.pageSize, 10, "Elemente pro Seite");
|
||||||
|
assert.strictEqual(res1.totalPages, 3, "25 Elemente bei pageSize 10 = 3 Seiten");
|
||||||
|
assert.strictEqual(res1.hasNextPage, true, "Seite 1 von 3 hat nächste Seite");
|
||||||
|
assert.strictEqual(res1.hasPrevPage, false, "Seite 1 hat keine vorherige Seite");
|
||||||
|
|
||||||
|
// Letzte Seite
|
||||||
|
const resLast = erstellePaginiertesErgebnis([{ id: 25, name: "Omega" }], 25, 3, 10);
|
||||||
|
assert.strictEqual(resLast.hasNextPage, false, "Letzte Seite hat hasNextPage=false");
|
||||||
|
assert.strictEqual(resLast.hasPrevPage, true, "Letzte Seite hat hasPrevPage=true");
|
||||||
|
|
||||||
|
// Leere Gesamtmenge
|
||||||
|
const resEmpty = erstellePaginiertesErgebnis([], 0, 1, 10);
|
||||||
|
assert.strictEqual(resEmpty.totalPages, 1, "0 Elemente ergeben 1 leere Seite");
|
||||||
|
assert.strictEqual(resEmpty.hasNextPage, false);
|
||||||
|
assert.strictEqual(resEmpty.hasPrevPage, false);
|
||||||
|
|
||||||
|
// Validierungstests
|
||||||
|
assertThrows(() => erstellePaginiertesErgebnis("kein-array", 10), "Items nicht als Array muss TypeError werfen");
|
||||||
|
assertThrows(() => erstellePaginiertesErgebnis([], -5), "Negatives Total muss RangeError werfen");
|
||||||
|
assertThrows(() => erstellePaginiertesErgebnis([], 10, 0), "Page 0 muss RangeError werfen");
|
||||||
|
assertThrows(() => erstellePaginiertesErgebnis([], 10, 1, 0), "PageSize 0 muss RangeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 2. Tests für istAdminBenutzer (Custom Type Guard)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const gueltigerAdmin = {
|
||||||
|
id: "ADM-007",
|
||||||
|
username: "superadmin",
|
||||||
|
email: "admin@system.corp",
|
||||||
|
rolle: "ADMIN",
|
||||||
|
berechtigungen: ["MANAGE_USERS", "DELETE_RECORDS"]
|
||||||
|
};
|
||||||
|
|
||||||
|
assert.strictEqual(istAdminBenutzer(gueltigerAdmin), true, "Gültiges Admin-Objekt muss true liefern");
|
||||||
|
|
||||||
|
const normalerUser = {
|
||||||
|
...gueltigerAdmin,
|
||||||
|
rolle: "USER"
|
||||||
|
};
|
||||||
|
assert.strictEqual(istAdminBenutzer(normalerUser), false, "User-Rolle darf kein Admin sein");
|
||||||
|
|
||||||
|
const adminOhneRechte = {
|
||||||
|
...gueltigerAdmin,
|
||||||
|
berechtigungen: []
|
||||||
|
};
|
||||||
|
assert.strictEqual(istAdminBenutzer(adminOhneRechte), false, "Admin ohne Berechtigungen muss abgelehnt werden");
|
||||||
|
|
||||||
|
const adminFalscheEmail = {
|
||||||
|
...gueltigerAdmin,
|
||||||
|
email: "kein-at-zeichen"
|
||||||
|
};
|
||||||
|
assert.strictEqual(istAdminBenutzer(adminFalscheEmail), false, "Ungültige E-Mail muss abgelehnt werden");
|
||||||
|
|
||||||
|
assert.strictEqual(istAdminBenutzer(null), false, "null muss false liefern");
|
||||||
|
assert.strictEqual(istAdminBenutzer(undefined), false, "undefined muss false liefern");
|
||||||
|
assert.strictEqual(istAdminBenutzer("admin"), false, "String muss false liefern");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 3. Tests für aktualisiereBenutzerProfil (Partial<T> Semantik)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const originalProfil = {
|
||||||
|
id: 101,
|
||||||
|
username: "ada_lovelace",
|
||||||
|
email: "ada@analytical.org",
|
||||||
|
einstellungen: {
|
||||||
|
darkMode: false,
|
||||||
|
benachrichtigungen: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updatePayload = {
|
||||||
|
username: " ada_the_first ",
|
||||||
|
einstellungen: {
|
||||||
|
darkMode: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const aktualisiert = aktualisiereBenutzerProfil(originalProfil, updatePayload);
|
||||||
|
|
||||||
|
// Prüfung der Werte
|
||||||
|
assert.strictEqual(aktualisiert.id, 101, "ID bleibt unverändert");
|
||||||
|
assert.strictEqual(aktualisiert.username, "ada_the_first", "Username aktualisiert und getrimmt");
|
||||||
|
assert.strictEqual(aktualisiert.email, "ada@analytical.org", "Nicht im Update enthaltene Felder bleiben erhalten");
|
||||||
|
assert.strictEqual(aktualisiert.einstellungen.darkMode, true, "darkMode wurde aktualisiert");
|
||||||
|
assert.strictEqual(aktualisiert.einstellungen.benachrichtigungen, true, "Verschachtelte Eigenschaften bleiben erhalten");
|
||||||
|
assert.ok(typeof aktualisiert.aktualisiertAm === "string", "aktualisiertAm Timestamp muss gesetzt sein");
|
||||||
|
|
||||||
|
// Immutability Prüfung: Original darf nicht verändert werden!
|
||||||
|
assert.strictEqual(originalProfil.username, "ada_lovelace", "Original darf nicht mutiert werden");
|
||||||
|
assert.strictEqual(originalProfil.einstellungen.darkMode, false, "Original-Einstellungen dürfen nicht mutiert werden");
|
||||||
|
|
||||||
|
// Validierungstests
|
||||||
|
assertThrows(() => aktualisiereBenutzerProfil(null, {}), "Original=null muss TypeError werfen");
|
||||||
|
assertThrows(() => aktualisiereBenutzerProfil(originalProfil, { email: "ungueltig" }), "Ungültige E-Mail muss TypeError werfen");
|
||||||
|
assertThrows(() => aktualisiereBenutzerProfil(originalProfil, { username: " " }), "Leerer Username muss TypeError werfen");
|
||||||
|
|
||||||
|
// ----------------------------------------------------
|
||||||
|
// 4. Tests für filtriereFelder (Pick<T, K> Semantik)
|
||||||
|
// ----------------------------------------------------
|
||||||
|
const komplexesObjekt = {
|
||||||
|
id: 99,
|
||||||
|
username: "linus",
|
||||||
|
passwortHash: "secret_salt_hash",
|
||||||
|
email: "linus@kernel.org",
|
||||||
|
ipAdresse: "192.168.1.1",
|
||||||
|
geheim: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const oeffentlichesProfil = filtriereFelder(komplexesObjekt, ["id", "username", "email"]);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
oeffentlichesProfil,
|
||||||
|
{ id: 99, username: "linus", email: "linus@kernel.org" },
|
||||||
|
"filtriereFelder sollte exakt die angegebenen Schlüssel kopieren"
|
||||||
|
);
|
||||||
|
assert.strictEqual("passwortHash" in oeffentlichesProfil, false, "Sensible Felder dürfen nicht enthalten sein");
|
||||||
|
|
||||||
|
// Nicht existierende Schlüssel
|
||||||
|
const gefiltertMitZusatz = filtriereFelder(komplexesObjekt, ["id", "nichtExistierend"]);
|
||||||
|
assert.deepStrictEqual(
|
||||||
|
gefiltertMitZusatz,
|
||||||
|
{ id: 99 },
|
||||||
|
"Nicht im Original existierende Schlüssel werden ausgelassen"
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(filtriereFelder(null, ["id"]), {}, "null als Objekt liefert {}");
|
||||||
|
|
||||||
|
console.log("✅ Alle 22 Tests für TS 15 (Fortgeschrittenes TypeScript & Generics) erfolgreich bestanden!");
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
|
|
||||||
<!-- Footer Navigation -->
|
<!-- Footer Navigation -->
|
||||||
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
|
<div class="footer-nav" style="display: flex; justify-content: space-between; margin-top: 30px;">
|
||||||
<a href="../07_einstieg_typescript/index.html" class="btn btn-secondary">← Zurück zu Modul 07</a>
|
<a href="../15_typescript_fortgeschritten/index.html" class="btn btn-secondary">← Zurück zu Modul 07</a>
|
||||||
<a href="../../../index.html" class="btn" style="background: var(--success); color: white;">🏆 Kurs abgeschlossen – Zur Übersicht →</a>
|
<a href="../../../index.html" class="btn" style="background: var(--success); color: white;">🏆 Kurs abgeschlossen – Zur Übersicht →</a>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
@ -4,38 +4,54 @@
|
||||||
"language": "javascript",
|
"language": "javascript",
|
||||||
"icon": "🌐",
|
"icon": "🌐",
|
||||||
"runner": "javascript",
|
"runner": "javascript",
|
||||||
"description": "Vom Browser-Scripting über ES6-Funktionen, Arrays bis hin zu TypeScript Type Safety und dem Master-Abschlussprojekt.",
|
"description": "Vom Einsteiger über moderne ES6+-Features, OOP, Reguläre Ausdrücke, Asynchronität bis hin zu TypeScript und dem Master-Abschlussprojekt.",
|
||||||
"tracks": [
|
"tracks": [
|
||||||
{
|
{
|
||||||
"id": "track_1_grundlagen",
|
"id": "track_1_grundlagen",
|
||||||
"title": "🌱 Lehrpfad 1: JavaScript Grundlagen & ES6",
|
"title": "🌱 Lehrpfad 1: JavaScript Grundlagen & ES6",
|
||||||
"description": "Variablen, Template Strings, Arrow Functions, Arrays, Objekte, Destructuring und DOM-Manipulation & Events.",
|
"description": "Variablen, Kontrollfluss, Funktionen, Scopes, Arrays, Sets, Maps, Objekte & Destructuring.",
|
||||||
"certificateKey": "js_grundlagen",
|
"certificateKey": "js_grundlagen",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{ "folder": "01_erste_schritte_variablen", "title": "JS 01: Variablen, Typen & Template-Strings", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
{ "folder": "01_erste_schritte_variablen", "title": "JS 01: Variablen, Typen & Template-Strings", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
{ "folder": "02_funktionen_und_arrays", "title": "JS 02: Arrow Functions, Arrays & Map/Filter", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
{ "folder": "02_kontrollfluss_und_schleifen", "title": "JS 02: Kontrollfluss & Schleifen", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
{ "folder": "03_objekte_und_destructuring", "title": "JS 03: Objekte, Destructuring & Rest/Spread", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
{ "folder": "03_funktionen_und_scope", "title": "JS 03: Funktionen, Arrow Functions & Scope", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
{ "folder": "04_dom_und_events", "title": "JS 04: DOM-Manipulation & Events", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
{ "folder": "04_arrays_sets_und_maps", "title": "JS 04: Arrays, Sets & Maps", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "05_objekte_und_destructuring", "title": "JS 05: Objekte, Destructuring & Rest/Spread", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_2_async_und_apis",
|
"id": "track_2_oop_und_daten",
|
||||||
"title": "⚡ Lehrpfad 2: Asynchrones JavaScript, Promises & REST-APIs",
|
"title": "🏗️ Lehrpfad 2: Moderne Datenmodelle & OOP",
|
||||||
"description": "Callbacks, Event Loop, Promises, Chaining, Promise.all, Async/Await und REST-APIs mit JSON.",
|
"description": "Objektorientierung mit ES6-Klassen, Reguläre Ausdrücke (RegExp) & strukturierte Fehlerbehandlung mit Error-Klassen.",
|
||||||
|
"certificateKey": "js_oop",
|
||||||
|
"chapters": [
|
||||||
|
{ "folder": "06_klassen_und_oop", "title": "JS 06: Klassen & Objektorientierung (OOP)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "07_regex_und_textverarbeitung", "title": "JS 07: Reguläre Ausdrücke & Textverarbeitung", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "08_fehlerbehandlung_und_debugging", "title": "JS 08: Fehlerbehandlung & Debugging", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "track_3_web_und_async",
|
||||||
|
"title": "⚡ Lehrpfad 3: Web-APIs, DOM & Asynchronität",
|
||||||
|
"description": "DOM-Manipulation, Web Storage, Callbacks, Promises, Async/Await und REST-APIs (Fetch).",
|
||||||
"certificateKey": "js_async",
|
"certificateKey": "js_async",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{ "folder": "05_callbacks_und_promises", "title": "JS 05: Callbacks, Event Loop & Promises", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
{ "folder": "09_dom_und_events", "title": "JS 09: DOM-Manipulation & Events", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
{ "folder": "06_async_await_und_fetch", "title": "JS 06: Async/Await & REST-APIs (Fetch)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
{ "folder": "10_web_storage_und_json", "title": "JS 10: Web Storage & JSON", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "11_callbacks_und_promises", "title": "JS 11: Callbacks, Event Loop & Promises", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "12_async_await_und_fetch", "title": "JS 12: Async/Await & REST-APIs (Fetch)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "track_3_typescript_und_master",
|
"id": "track_4_typescript_und_master",
|
||||||
"title": "🛡️ Lehrpfad 3: TypeScript & Master-Abschlussprojekt",
|
"title": "🛡️ Lehrpfad 4: Architektur, TypeScript & Masterprojekt",
|
||||||
"description": "Statische Typsicherheit, Interfaces, Generics, State-Management und vollwertige Web-Applikation.",
|
"description": "ES-Module, statische Typsicherheit mit TypeScript, Interfaces, Generics und vollwertige MVC-Applikation.",
|
||||||
"certificateKey": "js_master",
|
"certificateKey": "js_master",
|
||||||
"chapters": [
|
"chapters": [
|
||||||
{ "folder": "07_einstieg_typescript", "title": "TS 07: TypeScript Type Safety & Interfaces", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
{ "folder": "13_es_module_und_architektur", "title": "JS 13: ES-Module & Clean Architecture", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
{ "folder": "08_abschlussprojekt_web_app", "title": "Master 08: Interaktive Task-App (MVC)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
{ "folder": "14_einstieg_typescript", "title": "TS 14: TypeScript Type Safety & Interfaces", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "15_typescript_fortgeschritten", "title": "TS 15: TypeScript Generics & Advanced Types", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" },
|
||||||
|
{ "folder": "16_master_abschlussprojekt_web_app", "title": "Master 16: Interaktive Task-App (MVC)", "taskFile": "aufgabe.js", "testFile": "test_aufgabe.js" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@
|
||||||
<label style="font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; font-weight: 700; display: block; margin-bottom: 4px;">Aktiver Sprachkurs:</label>
|
<label style="font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; font-weight: 700; display: block; margin-bottom: 4px;">Aktiver Sprachkurs:</label>
|
||||||
<select id="course-selector-dropdown" style="width: 100%; background: #0f172a; color: #f8fafc; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 8px; font-size: 0.82rem; font-weight: 600;" onchange="window.location.href = this.value === 'python' ? 'index.html' : 'workspace.html?course=' + this.value + '&chapter=01_' + (this.value === 'javascript' ? 'erste_schritte_variablen' : 'tabellen_und_select')">
|
<select id="course-selector-dropdown" style="width: 100%; background: #0f172a; color: #f8fafc; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 8px; font-size: 0.82rem; font-weight: 600;" onchange="window.location.href = this.value === 'python' ? 'index.html' : 'workspace.html?course=' + this.value + '&chapter=01_' + (this.value === 'javascript' ? 'erste_schritte_variablen' : 'tabellen_und_select')">
|
||||||
<option value="python" selected>🐍 Python (33 Module)</option>
|
<option value="python" selected>🐍 Python (33 Module)</option>
|
||||||
<option value="javascript">🌐 JavaScript (Demo)</option>
|
<option value="javascript">🌐 JavaScript & TypeScript (16 Module)</option>
|
||||||
<option value="sql">🗄️ SQL & SQLite (Demo)</option>
|
<option value="sql">🗄️ SQL & SQLite (Demo)</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -444,7 +444,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Subgoals (Exakte Regex ohne Duplikate)
|
// 2. Subgoals (Exakte Regex ohne Duplikate)
|
||||||
const subgoals = aufgabeCode.match(/^# 🎯 TEILZIEL \d+ \(TODO \d+\): (.+)$/gm) || [];
|
const subgoals = aufgabeCode.match(/^(?:#|\/\/|\s*\*)\s*🎯\s*TEILZIEL\s*\d+[^:\n]*:\s*(.+)$/gm) || [];
|
||||||
const subContainer = document.getElementById("subgoals-list-container");
|
const subContainer = document.getElementById("subgoals-list-container");
|
||||||
const savedSubgoals = JSON.parse(localStorage.getItem(`subgoals_${chapterId}`) || "{}");
|
const savedSubgoals = JSON.parse(localStorage.getItem(`subgoals_${chapterId}`) || "{}");
|
||||||
|
|
||||||
|
|
@ -452,7 +452,7 @@
|
||||||
subContainer.innerHTML = subgoals.map((s, idx) => `
|
subContainer.innerHTML = subgoals.map((s, idx) => `
|
||||||
<div class="task-item" style="margin-bottom: 8px;">
|
<div class="task-item" style="margin-bottom: 8px;">
|
||||||
<input type="checkbox" class="task-checkbox" id="sub_check_${idx}" ${savedSubgoals[idx] ? 'checked' : ''}>
|
<input type="checkbox" class="task-checkbox" id="sub_check_${idx}" ${savedSubgoals[idx] ? 'checked' : ''}>
|
||||||
<label class="task-label" for="sub_check_${idx}"><strong>${s.replace(/^#\s*/, '')}</strong></label>
|
<label class="task-label" for="sub_check_${idx}"><strong>${s.replace(/^(?:#|\/\/|\s*\*)\s*/, '')}</strong></label>
|
||||||
</div>
|
</div>
|
||||||
`).join("");
|
`).join("");
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue