Callbacks in JavaScript verstehen
Callbacks in JavaScript verstehen
Callbacks sind das Fundament der asynchronen Programmierung in JavaScript. Bevor du Promises und async/await verstehen kannst, musst du Callbacks meistern.
Was ist ein Callback?
Ein Callback ist eine Funktion, die einer anderen Funktion als Argument übergeben wird und später ausgeführt wird.
// gruss ist der Callback
function sagHallo(name, callback) {
const nachricht = `Hallo ${name}!`;
callback(nachricht); // Callback aufrufen
}
// Funktion mit Callback aufrufen
sagHallo("Max", function(msg) {
console.log(msg); // "Hallo Max!"
});
// Mit Arrow Function
sagHallo("Lisa", msg => console.log(msg)); // "Hallo Lisa!"
Warum Callbacks?
1. Code wiederverwenden
// Eine Funktion, verschiedene Verhaltensweisen
function verarbeiteZahlen(zahlen, callback) {
const ergebnisse = [];
for (const zahl of zahlen) {
ergebnisse.push(callback(zahl));
}
return ergebnisse;
}
const zahlen = [1, 2, 3, 4, 5];
// Verdoppeln
const verdoppelt = verarbeiteZahlen(zahlen, x => x * 2);
console.log(verdoppelt); // [2, 4, 6, 8, 10]
// Quadrieren
const quadriert = verarbeiteZahlen(zahlen, x => x * x);
console.log(quadriert); // [1, 4, 9, 16, 25]
// Plus 10
const plus10 = verarbeiteZahlen(zahlen, x => x + 10);
console.log(plus10); // [11, 12, 13, 14, 15]
2. Asynchrone Operationen
JavaScript ist single-threaded, aber muss trotzdem auf langsame Operationen warten können (Netzwerk, Dateien, Timer). Callbacks ermöglichen das.
console.log("1. Starte");
// setTimeout ist asynchron - Callback wird später ausgeführt
setTimeout(function() {
console.log("2. Nach 2 Sekunden");
}, 2000);
console.log("3. Ende");
// Ausgabe:
// "1. Starte"
// "3. Ende"
// (2 Sekunden Pause)
// "2. Nach 2 Sekunden"
Synchrone Callbacks
Callbacks, die sofort ausgeführt werden:
Array-Methoden
const zahlen = [1, 2, 3, 4, 5];
// forEach - führt Callback für jedes Element aus
zahlen.forEach(function(zahl, index) {
console.log(`Index ${index}: ${zahl}`);
});
// map - transformiert jedes Element
const verdoppelt = zahlen.map(function(zahl) {
return zahl * 2;
});
// filter - behält Elemente die true zurückgeben
const gerade = zahlen.filter(function(zahl) {
return zahl % 2 === 0;
});
// find - findet erstes Element
const ersteGrosseAlsDrei = zahlen.find(function(zahl) {
return zahl > 3;
});
// reduce - reduziert auf einen Wert
const summe = zahlen.reduce(function(acc, curr) {
return acc + curr;
}, 0);
Mit Arrow Functions (moderner)
const zahlen = [1, 2, 3, 4, 5];
zahlen.forEach((zahl, i) => console.log(`${i}: ${zahl}`));
const verdoppelt = zahlen.map(z => z * 2);
const gerade = zahlen.filter(z => z % 2 === 0);
const erstesGrosse = zahlen.find(z => z > 3);
const summe = zahlen.reduce((acc, curr) => acc + curr, 0);
sort mit Callback
const namen = ["Clara", "Anna", "Ben", "David"];
// Alphabetisch (Standard)
namen.sort();
// ["Anna", "Ben", "Clara", "David"]
// Mit Callback - eigene Sortierung
const zahlen = [10, 2, 5, 1, 8];
// Aufsteigend
zahlen.sort((a, b) => a - b);
// [1, 2, 5, 8, 10]
// Absteigend
zahlen.sort((a, b) => b - a);
// [10, 8, 5, 2, 1]
// Objekte sortieren
const personen = [
{ name: "Max", alter: 25 },
{ name: "Lisa", alter: 30 },
{ name: "Tom", alter: 20 }
];
personen.sort((a, b) => a.alter - b.alter);
// Sortiert nach Alter aufsteigend
Asynchrone Callbacks
Callbacks, die später ausgeführt werden:
setTimeout und setInterval
// Nach 3 Sekunden ausführen
setTimeout(() => {
console.log("3 Sekunden vorbei!");
}, 3000);
// Alle 2 Sekunden ausführen
let count = 0;
const interval = setInterval(() => {
count++;
console.log(`Tick ${count}`);
if (count >= 5) {
clearInterval(interval); // Stoppen
console.log("Fertig!");
}
}, 2000);
Event Listener
const button = document.querySelector("#myButton");
// Callback wird bei jedem Klick ausgeführt
button.addEventListener("click", function(event) {
console.log("Button geklickt!");
console.log("Event:", event);
});
// Mit Arrow Function
button.addEventListener("click", (e) => {
console.log("Klick auf:", e.target);
});
// Input-Events
const input = document.querySelector("#myInput");
input.addEventListener("input", (e) => {
console.log("Neuer Wert:", e.target.value);
});
Datei lesen (Node.js Beispiel)
const fs = require("fs");
// Asynchron mit Callback
fs.readFile("datei.txt", "utf8", function(error, data) {
if (error) {
console.error("Fehler:", error);
return;
}
console.log("Inhalt:", data);
});
console.log("Diese Zeile kommt zuerst!");
Callback-Pattern: Error-First
Das Error-First Callback Pattern ist ein Standard in Node.js:
// Konvention: Erster Parameter ist immer error
function ladeDaten(id, callback) {
// Simuliere async Operation
setTimeout(() => {
if (id < 0) {
callback(new Error("Ungültige ID"), null);
return;
}
const daten = { id: id, name: "Max" };
callback(null, daten); // null = kein Fehler
}, 1000);
}
// Verwendung
ladeDaten(5, function(error, daten) {
if (error) {
console.error("Fehler:", error.message);
return;
}
console.log("Daten:", daten);
});
ladeDaten(-1, function(error, daten) {
if (error) {
console.error("Fehler:", error.message); // "Ungültige ID"
return;
}
console.log("Daten:", daten);
});
Die Callback-Hölle
Wenn mehrere asynchrone Operationen nacheinander ausgeführt werden müssen:
// 😱 Callback Hell - Pyramide des Todes
getUser(userId, function(error, user) {
if (error) {
handleError(error);
return;
}
getOrders(user.id, function(error, orders) {
if (error) {
handleError(error);
return;
}
getOrderDetails(orders[0].id, function(error, details) {
if (error) {
handleError(error);
return;
}
getShippingInfo(details.shippingId, function(error, shipping) {
if (error) {
handleError(error);
return;
}
// Endlich haben wir alle Daten...
console.log(user, orders, details, shipping);
});
});
});
});
Lösungen für Callback Hell
1. Benannte Funktionen
function handleShipping(error, shipping) {
if (error) return handleError(error);
console.log("Shipping:", shipping);
}
function handleDetails(error, details) {
if (error) return handleError(error);
getShippingInfo(details.shippingId, handleShipping);
}
function handleOrders(error, orders) {
if (error) return handleError(error);
getOrderDetails(orders[0].id, handleDetails);
}
function handleUser(error, user) {
if (error) return handleError(error);
getOrders(user.id, handleOrders);
}
// Viel flacher!
getUser(userId, handleUser);
2. Promises (moderne Lösung)
// Statt verschachtelter Callbacks
getUser(userId)
.then(user => getOrders(user.id))
.then(orders => getOrderDetails(orders[0].id))
.then(details => getShippingInfo(details.shippingId))
.then(shipping => console.log(shipping))
.catch(error => handleError(error));
3. Async/Await (modernste Lösung)
async function ladeAlles(userId) {
try {
const user = await getUser(userId);
const orders = await getOrders(user.id);
const details = await getOrderDetails(orders[0].id);
const shipping = await getShippingInfo(details.shippingId);
console.log(shipping);
} catch (error) {
handleError(error);
}
}
Eigene Callback-Funktionen erstellen
Einfacher Timer
function countdown(sekunden, onTick, onFinish) {
let verbleibend = sekunden;
const interval = setInterval(() => {
verbleibend--;
onTick(verbleibend);
if (verbleibend === 0) {
clearInterval(interval);
onFinish();
}
}, 1000);
}
// Verwendung
countdown(
5,
(sek) => console.log(`Noch ${sek} Sekunden...`),
() => console.log("FERTIG! 🎉")
);
Daten laden (simuliert)
function fetchData(url, onSuccess, onError) {
console.log(`Lade ${url}...`);
// Simuliere Netzwerk-Request
setTimeout(() => {
const zufallsZahl = Math.random();
if (zufallsZahl > 0.2) {
// 80% Erfolg
const daten = { url, data: "Hier sind die Daten!" };
onSuccess(daten);
} else {
// 20% Fehler
onError(new Error("Netzwerkfehler"));
}
}, 1500);
}
// Verwendung
fetchData(
"https://api.example.com/users",
(daten) => console.log("Erfolg:", daten),
(error) => console.error("Fehler:", error.message)
);
Animation
function animate(duration, onProgress, onComplete) {
const startTime = Date.now();
function step() {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / duration, 1);
onProgress(progress);
if (progress < 1) {
requestAnimationFrame(step);
} else {
onComplete();
}
}
requestAnimationFrame(step);
}
// Verwendung
const element = document.querySelector(".box");
animate(
2000, // 2 Sekunden
(progress) => {
// 0 bis 1
element.style.opacity = progress;
element.style.transform = `translateX(${progress * 200}px)`;
},
() => console.log("Animation fertig!")
);
Callback mit Kontext (this)
const controller = {
count: 0,
// Problem: this geht verloren
startBroken: function() {
setInterval(function() {
this.count++; // this ist nicht controller!
console.log(this.count); // NaN
}, 1000);
},
// Lösung 1: Arrow Function
startArrow: function() {
setInterval(() => {
this.count++; // Arrow erbt this
console.log(this.count);
}, 1000);
},
// Lösung 2: bind
startBind: function() {
setInterval(function() {
this.count++;
console.log(this.count);
}.bind(this), 1000);
},
// Lösung 3: self/that
startSelf: function() {
const self = this;
setInterval(function() {
self.count++;
console.log(self.count);
}, 1000);
}
};
Praktisches Projekt: Async Queue
function createQueue() {
const tasks = [];
let running = false;
function processNext() {
if (tasks.length === 0) {
running = false;
return;
}
running = true;
const { task, callback } = tasks.shift();
task((error, result) => {
callback(error, result);
processNext(); // Nächste Task
});
}
return {
add: function(task, callback) {
tasks.push({ task, callback });
if (!running) {
processNext();
}
},
length: function() {
return tasks.length;
}
};
}
// Verwendung
const queue = createQueue();
function asyncTask(name, delay) {
return function(done) {
console.log(`Starting: ${name}`);
setTimeout(() => {
console.log(`Finished: ${name}`);
done(null, `Result from ${name}`);
}, delay);
};
}
queue.add(asyncTask("Task 1", 1000), (err, res) => console.log(res));
queue.add(asyncTask("Task 2", 500), (err, res) => console.log(res));
queue.add(asyncTask("Task 3", 800), (err, res) => console.log(res));
// Ausgabe (nacheinander, nicht parallel):
// Starting: Task 1
// Finished: Task 1
// Result from Task 1
// Starting: Task 2
// ...
Zusammenfassung
| Konzept | Beschreibung |
|---|---|
| Callback | Funktion als Argument übergeben |
| Synchron | forEach, map, filter - sofort ausgeführt |
| Asynchron | setTimeout, Events - später ausgeführt |
| Error-First | callback(error, result) - Node.js Standard |
| Callback Hell | Verschachtelte Callbacks - vermeiden! |
// Synchroner Callback
array.forEach(item => console.log(item));
// Asynchroner Callback
setTimeout(() => console.log("später"), 1000);
// Error-First Pattern
function async(callback) {
// callback(error, result)
callback(null, "Erfolg");
}
Übung: Erstelle eine retry Funktion, die eine asynchrone Operation bis zu 3 Mal versucht, bevor sie aufgibt. retry(asyncOperation, maxAttempts, callback)