JavaScript/Tutorials/Umgang mit Callback-Funktionen

Aus SELFHTML-Wiki
Wechseln zu: Navigation, Suche

Eine Rückruffunktion (engl. callback function) ist eine Funktion, die einer anderen Funktion als Parameter übergeben und von dieser erst später unter definierten Bedingungen mit definierten Argumenten aufgerufen wird.

Diese Anleitung behandelt den Umgang mit Callback-Funktionen, wie sie in verschiedenen Kontexten in JavaScript Verwendung finden, z. B. bei den Array-Methoden map und forEach.

Funktionen als Container mit eigenem Scope

In JavaScript hat eine Funktion einen eigenen Geltungsbereich für Variablen (Scope). Zusätzlich gelten in einer Funktion aber auch alle die Variablen, die im Scope oberhalb der Funktion definiert sind (wenn man die Funktionsdefinition/-deklaration ebenfalls wie eine lokale Variable versteht).

"Scope in JavaScript"
// "globaler" Scope
var a = 7, b = 9;
// window.a ist jetzt 7, window.b ist jetzt 9

function quadrat (zahl) { return zahl*zahl; }
// kann jetzt auch mit window.quadrat() aufgerufen werden

function container (c) {
    // c ist eine lokale Variable, die ab hier in dieser Funktion verfügbar ist.

    // a und b sind ebenfalls verfügbar, weil sie im übergeordneten Scope notiert sind.
    // a (window.a) ist noch 7
    // b (window.b) ist noch 9

    function a_b_aendern () {
        // a und b sind auch hier wieder verfügbar, ebenso c.
        a = 8;
        b = 10;
    }
    // In der Funktion "container" gibt es jetzt eine lokale Funktion "a_b_aendern".
    // Sie kann nur innerhalb der Funktion "container" aufgerufen werden.
    // Man hätte auch var a_b_aendern = function () {...} notieren können.

    a_b_aendern(); // ändert auf a (window.a) = 8 und b (window.b) = 10

    var summe = function (a, b) {
        /* hier gibt es jetzt zwei neue lokale Variablen a und b, die nichts
           mehr mit window.a und window.b zu tun haben! */
        // c ist hier verfügbar.

        return a + b;
    }
    // In der Funktion "container" gibt es jetzt eine lokale Funktion "summe".
    // Sie kann nur innerhalb der Funktion "container" aufgerufen werden.
    // Man hätte auch function summe (a, b) {...} notieren können.

    c = summe(a, b) * c; // entspricht (window.a + window.b) * c
    /* Bei diesem Aufruf von Summe werden die Werte in a (jetzt 8) und b (jetzt 10)
       in der Funktion "summe" addiert, die ihrerseits lokale Variablen für die
       Summanden benutzt, die zwar auch a und b heißen, in dieser Funktion aber
       andere lokale Variablen sind. */

    return c;
}

container(20); // 360

Kapselung von Werten in Funktionen

Da es in JavaScript Ereignisse gibt, deren Funktionalität auch wieder über Funktionsaufrufe definiert werden, können Situationen entstehen, in denen Werte verfügbar sein müssen, obwohl ihre "Herstellung" längst abgeschlossen ist und eigentlich die Variablen nicht mehr existieren dürften. Hier kommt nun das Kapseln von Werten in einer Funktion zum Einsatz, einer sogenannten Closure, also einer Funktion, innerhalb derer ein Wert in einer Variable noch bekannt ist, obwohl der übergeordnete Kontext längst abgearbeitet wurde.

"Closure"
function setup (str) {
    var el = document.getElementById("myButton");

    if (el) {
        el.onclick = function () {
            // hier ist str verfügbar
            alert(str);
        }
    }
}

setup("Danke, dass Sie geklickt haben.");

In diesem Beispiel wird eine Funktion setup definiert, um ein Element mit der ID myButton mit einem click-Ereignis auszustatten. Dazu wird der onclick-Eigenschaft des Elements im DOM, welches die Methode document.getElementById() zurückliefert, eine Funktion (genauer ein Funktionsobjekt) als Wert zugewiesen. Diese Funktion wird ausgeführt, wenn auf das Element geklickt wird.

Wird nun die Funktion durch das click-Ereignis ausgeführt, so "erinnert" sie sich an die Variable str und kann ihren Wert mit dem alert-Aufruf an den Benutzer ausgeben, obwohl die Ausführung der Funktion setup längst abgeschlossen ist! Sie ist eine Closure.

Beachten Sie: Aus Gründen der Einfachheit ist die onclick-Eigenschaft des Elements verwendet worden, was nicht unproblematisch ist. Besser wäre es die Ereignisbehandlung dynamisch anzuhängen.

Callback-Funktionen als Kapselungen

Callback-Funktionen helfen nicht nur dabei, eine Notation einer herkömmlichen Schleife zu vermeiden und den Code übersichtlicher zu gestalten, sie können auch Variablen einschließen.

Nehmen wir einmal an, wir wollten ein click-Ereignis auf jeden Textabsatz eines Dokuments mit einer Ausgabe beantworten und täten das auf herkömmliche Art mittels einer Schleife:

"kaputt" ansehen …
function nerveBesucher () {
    var p = document.getElementsByTagName("p"), i;

    for (i = 0; i < p.length; i++) {
        p[i].onclick = function () {
            alert("Sie haben auf den " + (i+1) + ". Absatz geklickt!");
        };
    }
}

Wenn obige Funktion nerveBesucher nach dem vollständigen Laden des Dokuments ausgeführt wird, so werden alle Textabsätze in der Variable p gesammelt. In dieser Liste (es ist ein Objekt vom Typ HTML-Collection, entfernt ähnlich wie ein Array) befinden sich nun Referenzen auf die jeweiligen p-Objektelemente, über die in der for-Schleife nun iteriert wird.

In der gespeicherten Funktion wird nun ein Popup-Fenster des Browsers geöffnet, in dem neben dem OK-Button auch der Hinweis steht, auf den wievielten Textabsatz der Anwender geklickt hat. Dazu wird in der Funktion die Variable i benutzt, welche auch im übergeordneten Geltungsbereich verfügbar ist. Das führt zu dem Problem, dass jeder Hinweis auf den letzten Absatz verweist, egal auf welchen der Anwender gerade geklickt hat. Das Problem ist die Variable i, die innerhalb der Schleife natürlich kontinuierlich nach oben gezählt wird. Da nun aber jede gespeicherte Funktion auf diese Variable zugreift (closure!), zeigen alle dieselbe Nummer des vermeintlich geklickten Textabsatzes.

Abhilfe würde folgendes Konstrukt schaffen, welches das Zuweisen der click-Eigenschaft in eine eigene Funktion kapselt:

"umständlich" ansehen …
function nerveBesucher () {
    var p = document.getElementsByTagName("p"), i;

    for (i = 0; i < p.length; i++) {
        (function (_p, _i) {
            _p.onclick = function () {
                alert("Sie haben auf den " + _i + ". Absatz geklickt!");
            };
        })(p[i], i+1);
    }
}

In diesem Beispiel wird innerhalb der for-Schleife eine anonyme Funktion notiert, die in runde Klammern gefasst zu einem Ausdruck wird, der mit dem folgenden Parameter-Paar (p[i] und i+1) als Funktionsaufruf genutzt werden kann. Damit sind gleich zwei neue lokale Variablen in der onclick-Funktion vorhanden (_p und _i), die nur innerhalb dieser gelten. Nun wird jeder Textabsatz mit der korrekten Zahl ausgegeben.

Dem aufmerksamen Leser wird nicht entgangen sein, dass die Notation (function (a, b) {...})(x, y) eher eine Notlösung als eine gut leserliche Schreibweise ist. Hier kommt nun die Verwendung einer Callback-Funktion zum Einsatz, um das obige Konstrukt leichter verständlich zu notieren:

"elegant" ansehen …
function nerveBesucher () {
    var ps = document.querySelectorAll("p");

    if (ps.forEach) {

        // alle gängigen aktuellen Browser
        ps.forEach(function (p, i) {
            p.onclick = function () {
                alert("Sie haben auf den " + (i+1) + ". Absatz geklickt!");
            };
        });

    } else {

        // für IE
        Array.prototype.forEach(ps, function (p, i) {
            p.onclick = function () {
                alert("Sie haben auf den " + (i+1) + ". Absatz geklickt!");
            };
        });
    }
}
Beachten Sie: Anders als die HTML-Collection, die von der getElementsByTagName-Methode zurück geliefert wird, hat die live NodeList eine forEach-Methode. Daher verwenden wir in diesem Beispiel nicht mehr getElementsByTagName, sondern querySelectorAll, welches als Rückgabewert eine solche live NodeList liefert.
Die elegante Methode ist es nun, die forEach-Methode dieser live NodeList zu benutzen. Leider aber hat diese im Internet Explorer keine forEach-Methode. Um nun auch den Internet Explorer zu unterstützen, wäre die forEach-Methode des Array-Prototypen explizit auf die live NodeList anzuwenden. Dazu muss man zuerst die live NodeList als erstes Argument und die Callback-Funktion als zweites Argument notieren.

Siehe auch