Callbacks in JavaScript verstehen
Was sind Callbacks und warum brauchen wir sie? Lerne das Fundament der asynchronen Programmierung mit praktischen Beispielen.
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)