8 Kontrollstrukturen und Schleifen

Kontrollstrukturen und Schleifen sind fundamentale Bausteine jeder Programmiersprache. Sie ermöglichen es, den Programmfluss zu steuern, Entscheidungen zu treffen und Codeblöcke wiederholt auszuführen. JavaScript bietet eine Vielzahl solcher Konstrukte, die wir in diesem Abschnitt detailliert betrachten werden.

8.1 Bedingte Anweisungen

8.1.1 if-Anweisung

Die if-Anweisung ist die grundlegendste Kontrollstruktur und führt einen Codeblock aus, wenn eine Bedingung als wahr (true) ausgewertet wird:

if (bedingung) {
  // Dieser Code wird nur ausgeführt, wenn bedingung true ergibt
}

Da JavaScript dynamische Typisierung verwendet, wird die Bedingung automatisch in einen booleschen Wert umgewandelt. Wie im Kapitel “Variablen, Datentypen und Typkonvertierung” beschrieben, werden dabei bestimmte Werte als “falsy” betrachtet:

// Diese Bedingungen sind alle "falsy"
if (false) {}
if (0) {}
if ("") {}
if (null) {}
if (undefined) {}
if (NaN) {}

// Alle anderen Werte sind "truthy"
if (true) {}
if (42) {}
if ("text") {}
if ({}) {}
if ([]) {}

8.1.2 if-else-Anweisung

Mit else kann ein alternativer Codeblock ausgeführt werden, wenn die Bedingung nicht erfüllt ist:

if (alter >= 18) {
  console.log("Volljährig");
} else {
  console.log("Minderjährig");
}

8.1.3 if-else if-else-Kette

Für mehrere alternative Bedingungen kann die if-else if-else-Struktur verwendet werden:

if (note >= 90) {
  console.log("Sehr gut");
} else if (note >= 80) {
  console.log("Gut");
} else if (note >= 70) {
  console.log("Befriedigend");
} else if (note >= 60) {
  console.log("Ausreichend");
} else {
  console.log("Nicht bestanden");
}

8.1.4 Geschachtelte if-Anweisungen

Bedingte Anweisungen können beliebig geschachtelt werden, was jedoch die Lesbarkeit beeinträchtigen kann:

if (isAuthenticated) {
  if (userRole === "admin") {
    console.log("Admin-Bereich");
  } else {
    console.log("Benutzerbereich");
  }
} else {
  console.log("Bitte anmelden");
}

In solchen Fällen kann häufig eine flachere Struktur mit logischen Operatoren oder eine frühe Rückkehr die Lesbarkeit verbessern:

// Mit logischen Operatoren
if (isAuthenticated && userRole === "admin") {
  console.log("Admin-Bereich");
} else if (isAuthenticated) {
  console.log("Benutzerbereich");
} else {
  console.log("Bitte anmelden");
}

// Mit früher Rückkehr (in einer Funktion)
function checkAccess() {
  if (!isAuthenticated) {
    console.log("Bitte anmelden");
    return;
  }
  
  if (userRole === "admin") {
    console.log("Admin-Bereich");
  } else {
    console.log("Benutzerbereich");
  }
}

8.1.5 Bedingte (ternäre) Operatoren

Für einfache bedingte Zuweisungen bietet der ternäre Operator eine kompakte Alternative:

// Langform mit if-else
let status;
if (alter >= 18) {
  status = "erwachsen";
} else {
  status = "minderjährig";
}

// Kompakte Form mit ternärem Operator
let status = alter >= 18 ? "erwachsen" : "minderjährig";

Bei komplexeren Bedingungen sollte jedoch der Lesbarkeit Vorrang gegeben werden.

8.1.6 switch-Anweisung

Die switch-Anweisung eignet sich für Mehrfachverzweigungen basierend auf einem einzelnen Wert:

switch (wochentag) {
  case "Montag":
    console.log("Wochenanfang");
    break;
  case "Dienstag":
  case "Mittwoch":
  case "Donnerstag":
    console.log("Wochenmitte");
    break;
  case "Freitag":
    console.log("Fast Wochenende");
    break;
  case "Samstag":
  case "Sonntag":
    console.log("Wochenende");
    break;
  default:
    console.log("Ungültiger Wochentag");
}

Wichtig ist das break-Statement, das verhindert, dass die Ausführung in den nächsten case fällt (der sogenannte “Fall-Through”). In einigen Fällen kann ein bewusstes Weglassen von break nützlich sein, um denselben Code für mehrere Fälle auszuführen, wie im obigen Beispiel zu sehen.

Die switch-Anweisung verwendet strenge Gleichheit (===), was bei der Arbeit mit unterschiedlichen Datentypen zu beachten ist.

8.2 Schleifen

Schleifen ermöglichen die wiederholte Ausführung von Code und sind unerlässlich für die Arbeit mit Datensammlungen und iterative Algorithmen.

8.2.1 for-Schleife

Die klassische for-Schleife besteht aus drei Teilen: Initialisierung, Bedingung und Inkrement:

for (let i = 0; i < 5; i++) {
  console.log(`Durchlauf ${i}`);
}

Die Einzelteile können auch weggelassen werden (führt zu einer Endlosschleife, wenn keine Abbruchbedingung implementiert wird):

let i = 0;
for (;;) {
  if (i >= 5) break;
  console.log(`Durchlauf ${i}`);
  i++;
}

8.2.2 for…in-Schleife

Die for...in-Schleife iteriert über die enumerierbaren Eigenschaften eines Objekts, einschließlich seiner Prototypenkette:

const person = {
  name: "Max",
  alter: 30,
  beruf: "Entwickler"
};

for (const eigenschaft in person) {
  console.log(`${eigenschaft}: ${person[eigenschaft]}`);
}
// Ausgabe:
// name: Max
// alter: 30
// beruf: Entwickler

Wichtig: for...in ist für Objekte gedacht und sollte nicht für Arrays verwendet werden, da die Reihenfolge nicht garantiert ist und auch Eigenschaften der Prototypenkette erfasst werden können.

8.2.3 for…of-Schleife (ES6)

Die for...of-Schleife wurde mit ES6 eingeführt und iteriert über iterierbare Objekte (Arrays, Strings, Maps, Sets usw.):

const zahlen = [10, 20, 30, 40, 50];

for (const zahl of zahlen) {
  console.log(zahl);
}
// Ausgabe: 10, 20, 30, 40, 50

const text = "JavaScript";
for (const buchstabe of text) {
  console.log(buchstabe);
}
// Ausgabe: J, a, v, a, S, c, r, i, p, t

Im Gegensatz zu for...in berücksichtigt for...of nur die Werte und nicht die Indizes oder Schlüssel.

8.2.4 Vergleich: for…in vs. for…of

const array = ["a", "b", "c"];
array.zusatz = "nicht iterierbar mit for...of";

// for...in gibt Indizes UND zusätzliche Eigenschaften aus
for (const index in array) {
  console.log(index); // 0, 1, 2, zusatz
}

// for...of gibt nur iterierbare Werte aus
for (const wert of array) {
  console.log(wert); // a, b, c
}

8.2.5 while-Schleife

Die while-Schleife führt einen Codeblock aus, solange eine Bedingung erfüllt ist:

let count = 0;
while (count < 5) {
  console.log(`Zähler: ${count}`);
  count++;
}

Die Bedingung wird vor jeder Iteration geprüft. Ist sie bereits zu Beginn false, wird der Schleifenkörper nie ausgeführt.

8.2.6 do…while-Schleife

Die do...while-Schleife ist ähnlich wie while, prüft die Bedingung jedoch erst nach der Ausführung des Schleifenkörpers:

let count = 0;
do {
  console.log(`Zähler: ${count}`);
  count++;
} while (count < 5);

Dadurch wird der Schleifenkörper mindestens einmal ausgeführt, auch wenn die Bedingung bereits zu Beginn false ist:

let count = 10;
do {
  console.log("Dieser Text wird trotz count >= 5 einmal ausgegeben");
} while (count < 5);

8.3 Schleifensteuerung

JavaScript bietet Anweisungen zur expliziten Steuerung des Schleifenverhaltens:

8.3.1 break-Anweisung

Mit break wird die innerste umgebende Schleife sofort verlassen:

for (let i = 0; i < 10; i++) {
  if (i === 5) {
    break; // Beendet die Schleife vorzeitig
  }
  console.log(i);
}
// Ausgabe: 0, 1, 2, 3, 4

8.3.2 continue-Anweisung

Mit continue wird die aktuelle Iteration abgebrochen und mit der nächsten fortgefahren:

for (let i = 0; i < 10; i++) {
  if (i % 2 === 0) {
    continue; // Überspringt gerade Zahlen
  }
  console.log(i);
}
// Ausgabe: 1, 3, 5, 7, 9

8.3.3 Labeled Statements

Mit Labels können Schleifen benannt werden, um sie gezielt mit break oder continue anzusprechen:

äußereSchleife: for (let i = 0; i < 3; i++) {
  innereSchleife: for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) {
      break äußereSchleife; // Beendet beide Schleifen
    }
    console.log(`i=${i}, j=${j}`);
  }
}

Dieses Feature wird in der Praxis selten verwendet, kann aber in komplexen verschachtelten Schleifen nützlich sein.

8.4 Moderne Alternativen zu Schleifen

In modernem JavaScript gibt es funktionale Alternativen zu traditionellen Schleifen, insbesondere für die Arbeit mit Arrays:

8.4.1 Array.prototype.forEach()

Die forEach-Methode führt eine Funktion für jedes Element eines Arrays aus:

const zahlen = [1, 2, 3, 4, 5];

zahlen.forEach((zahl, index) => {
  console.log(`Index ${index}: ${zahl}`);
});

Im Gegensatz zu einer Schleife kann forEach nicht mit break oder continue gesteuert werden - die Funktion wird für jedes Element ausgeführt.

8.4.2 Array.prototype.map()

Die map-Methode erstellt ein neues Array mit den Ergebnissen des Aufrufs einer Funktion für jedes Element:

const zahlen = [1, 2, 3, 4, 5];
const quadriert = zahlen.map(zahl => zahl * zahl);
console.log(quadriert); // [1, 4, 9, 16, 25]

8.4.3 Array.prototype.filter()

Die filter-Methode erstellt ein neues Array mit allen Elementen, die eine Bedingung erfüllen:

const zahlen = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const geradeZahlen = zahlen.filter(zahl => zahl % 2 === 0);
console.log(geradeZahlen); // [2, 4, 6, 8, 10]

8.4.4 Array.prototype.reduce()

Die reduce-Methode reduziert ein Array auf einen einzelnen Wert, indem sie eine Akkumulatorfunktion auf jedes Element anwendet:

const zahlen = [1, 2, 3, 4, 5];
const summe = zahlen.reduce((akkumulator, aktuellerWert) => {
  return akkumulator + aktuellerWert;
}, 0);
console.log(summe); // 15

// Komplexeres Beispiel: Zählen von Elementen nach Kategorie
const früchte = ['Apfel', 'Banane', 'Apfel', 'Orange', 'Banane', 'Apfel'];
const zählung = früchte.reduce((acc, frucht) => {
  acc[frucht] = (acc[frucht] || 0) + 1;
  return acc;
}, {});
console.log(zählung); // { Apfel: 3, Banane: 2, Orange: 1 }

8.4.5 Array.prototype.find() und findIndex()

Die find-Methode gibt das erste Element zurück, das eine Bedingung erfüllt:

const benutzer = [
  { id: 1, name: "Max" },
  { id: 2, name: "Anna" },
  { id: 3, name: "Tim" }
];

const gefundenerBenutzer = benutzer.find(user => user.id === 2);
console.log(gefundenerBenutzer); // { id: 2, name: "Anna" }

// findIndex gibt den Index statt des Elements zurück
const index = benutzer.findIndex(user => user.id === 2);
console.log(index); // 1

8.4.6 Array.prototype.some() und every()

Die some-Methode prüft, ob mindestens ein Element eine Bedingung erfüllt:

const zahlen = [1, 2, 3, 4, 5];
const hatGerade = zahlen.some(zahl => zahl % 2 === 0);
console.log(hatGerade); // true

Die every-Methode prüft, ob alle Elemente eine Bedingung erfüllen:

const zahlen = [1, 2, 3, 4, 5];
const allePositiv = zahlen.every(zahl => zahl > 0);
console.log(allePositiv); // true

const alleGerade = zahlen.every(zahl => zahl % 2 === 0);
console.log(alleGerade); // false

8.5 Performancebetrachtungen

Die Wahl der richtigen Schleife oder Iteration kann Auswirkungen auf die Performance haben:

  1. Klassische for-Schleifen sind in der Regel am performantesten, besonders bei großen Arrays.
  2. for...of ist etwas langsamer, aber sehr leserlich und für die meisten Anwendungen ausreichend schnell.
  3. forEach und andere Array-Methoden haben einen kleinen Overhead durch die Funktionsaufrufe.
  4. for...in ist am langsamsten und sollte für Arrays vermieden werden.

Ein einfaches Beispiel zur Demonstration:

const großesArray = Array(1000000).fill(0).map((_, i) => i);

console.time('for');
for (let i = 0; i < großesArray.length; i++) {
  // Verarbeitung
}
console.timeEnd('for');

console.time('for...of');
for (const element of großesArray) {
  // Verarbeitung
}
console.timeEnd('for...of');

console.time('forEach');
großesArray.forEach(element => {
  // Verarbeitung
});
console.timeEnd('forEach');

8.6 Typische Anwendungsmuster

8.6.1 Iteration über Arrays

const namen = ["Max", "Anna", "Tim"];

// Klassisch
for (let i = 0; i < namen.length; i++) {
  console.log(namen[i]);
}

// Moderner und lesbarer
for (const name of namen) {
  console.log(name);
}

// Funktional mit Zugriff auf Index
namen.forEach((name, index) => {
  console.log(`${index + 1}. ${name}`);
});

8.6.2 Iteration über Objekte

const person = {
  name: "Anna",
  alter: 28,
  beruf: "Entwicklerin"
};

// Mit for...in
for (const eigenschaft in person) {
  console.log(`${eigenschaft}: ${person[eigenschaft]}`);
}

// Mit Object.entries() (ES8)
for (const [eigenschaft, wert] of Object.entries(person)) {
  console.log(`${eigenschaft}: ${wert}`);
}

// Mit Object.keys()
Object.keys(person).forEach(eigenschaft => {
  console.log(`${eigenschaft}: ${person[eigenschaft]}`);
});

8.6.3 Iteration über Maps und Sets (ES6)

// Map
const benutzerRollen = new Map([
  ["max", "admin"],
  ["anna", "editor"],
  ["tim", "user"]
]);

for (const [benutzer, rolle] of benutzerRollen) {
  console.log(`${benutzer} ist ${rolle}`);
}

benutzerRollen.forEach((rolle, benutzer) => {
  console.log(`${benutzer} ist ${rolle}`);
});

// Set
const uniqueNumbers = new Set([1, 2, 3, 2, 1]);

for (const num of uniqueNumbers) {
  console.log(num); // 1, 2, 3
}

uniqueNumbers.forEach(num => {
  console.log(num); // 1, 2, 3
});

8.7 Asynchrone Iterationen (ES2018)

Mit ES2018 wurde asynchrone Iteration eingeführt, die besonders nützlich für asynchrone Datenströme ist:

async function processItems() {
  const items = getAsyncItemsSource();
  
  for await (const item of items) {
    // Verarbeite jedes Item asynchron
    await processItem(item);
  }
}

Dieses Thema wird im Kapitel “Asynchrones JavaScript” ausführlicher behandelt.

8.8 Beste Praktiken

  1. Klarheit vor Kürze: Wählen Sie die leserlichste Variante für den jeweiligen Anwendungsfall.
  2. Immutabilität bevorzugen: Nutzen Sie map, filter und andere Methoden, die neue Arrays erstellen, statt bestehende zu modifizieren.
  3. Frühe Abbrüche: Verwenden Sie break oder return, um Schleifen frühzeitig zu beenden, wenn das Ziel erreicht ist.
  4. Verschachtelung vermeiden: Tiefe Schleifenverschachtelungen verschlechtern die Lesbarkeit - extrahieren Sie lieber Funktionen.
  5. Endlosschleifen verhindern: Stellen Sie sicher, dass Ihre Schleifenbedingungen irgendwann false werden.
// Schlecht: Tiefe Verschachtelung
for (let i = 0; i < rows.length; i++) {
  for (let j = 0; j < columns.length; j++) {
    for (let k = 0; k < depth.length; k++) {
      // Komplexe Verarbeitung
    }
  }
}

// Besser: Extraktion in Funktion
function processCell(row, column, depth) {
  // Verarbeitung
}

for (let i = 0; i < rows.length; i++) {
  for (let j = 0; j < columns.length; j++) {
    for (let k = 0; k < depth.length; k++) {
      processCell(rows[i], columns[j], depth[k]);
    }
  }
}

// Noch besser: Funktionale Ansätze nutzen
rows.forEach(row => {
  columns.forEach(column => {
    depth.forEach(depth => {
      processCell(row, column, depth);
    });
  });
});