JavaScript Fortgeschritten

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

KonzeptBeschreibung
CallbackFunktion als Argument übergeben
SynchronforEach, map, filter - sofort ausgeführt
AsynchronsetTimeout, Events - später ausgeführt
Error-Firstcallback(error, result) - Node.js Standard
Callback HellVerschachtelte 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)