Wypróbuj

Nozbe - get things done

i zwiększ swoją produktywność

JavaScript – przekazywanie metody obiektu jako callback

6

Hej! Dziś trochę o moim ulubionym języku JavaScript, a konkretniej o dość popularnej konstrukcji tego języka jakim są wywołania zwrotne czyli inaczej, z angielska, o funkcjach callback. Jak możecie przeczytać w tytule tego wpisu, jest on poświęcony pewnemu szczególnemu przypadkowi funkcji callback. Chodzi mianowicie o przekazywanie metody obiektu jako callback. Innymi słowy jest to wywołanie zwrotne, które jednocześnie jest metodą obiektu. Zanim jednak przejdę do omówienia tego konkretnego przypadku, kilka słów przypomnienia co to jest funkcja callback i do czego może nam posłużyć.

Co to jest wywołanie zwrotne (callback) w Javascript?

Generalnie, callback jest to funkcja przekazywana jako parametr innej funkcji w celu późniejszego jej wywołania w tejże funkcji. Hmm… wyszło mi trochę masło maślane 🙂 lepiej więc może będzie pokazać to na prostym przykładzie:

function doItWhenReady() {
    alert('Hello from callback!');
}

function process(callback) {
    if (typeof callback !== 'function') {
         callback = false;
    }

    if (callback) {
         callback();
    }
}

process(doItWhenReady);

W pierwszych trzech liniach powyższego przykładu, widzimy zwykłą funkcję wyświetlającą jakiś tekst na ekranie. Ciekawsza jest druga z funkcji – „process”. Jak widać, przyjmuje ona jako parametr wejściowy zmienną „callback”. Jako, że nie wiemy czym tak naprawdę jest ta zmienna, dokonujemy sprawdzenia typu tej zmiennej i w przypadku gdy nie jest to funkcja, ustawiamy jej wartość na „false” – jest to dobra praktyka w przypadku tworzenia wywołań zwrotnych, ponieważ nie powinniśmy z góry zakładać, że ktoś przekaże nam jako parametr funkcję, a nie obiekt czy typ prosty. Dalej, będąc już pewnym, że zmienna „callback” jest funkcją, możemy ją wywołać tak jak wywołuje się każdą inną funkcję (zaznaczona linia). W ostatniej linii przykładu widzimy wywołanie funkcji „process” – jako parametr przekazujemy jej nazwę funkcji, która ma zostać wywołana w jej wnętrzu ale bez nawiasów – jeśli byśmy je dodali, funkcja „doItWhenReady” wywołałaby się natychmiast, w momencie wywołania funkcji „process”, a my chcemy wywołać ją przecież później. Tutaj znajdziecie jsfiddle dla tego przykładu.

Callback jako funkcja anonimowa

Oczywiście możemy również jako callback, przekazać funkcję anonimową. Poniżej modyfikacja poprzedniego wywołania (a tutaj jsfiddle dla niedowiarków):

process(function () {
    alert('Hello from callback!');
});

Funkcje callback są bardzo przydatne w wielu sytuacjach. Bardzo łatwo dzięki nim tworzyć własne biblioteki, które są rozszerzalne właśnie dzięki mechanizmowi wywołań zwrotnych. Jest to też jeden z podstawowych mechanizmów wykorzystywanych przy obsłudze zdarzeń – przecież wywołując funkcję „addEventListener”, oprócz nazwy zdarzenia przekazujemy jej również funkcję, która ma zareagować na dane zdarzenie. Przykłady użycia tego wzorca można by pewnie mnożyć, warto więc dobrze go znać i wiedzieć jak go stosować.

Przekazywanie metody obiektu jako callback

No to przechodzimy do meritum dzisiejszego postu. Opisane powyżej rozwiązanie sprawdza się w większości przypadków, jednak czasami zachodzi potrzeba przekazania jako callback funkcji będącej jednocześnie metodą jakiegoś obiektu. Dopóki metoda taka, nie odnosi się do składowych swojego obiektu jest wszystko OK, jednak jeśli znajdzie się w niej odwołanie z użyciem „this”, sprawa nie wygląda już tak prosto. Zmodyfikuję teraz nieco poprzedni przykład:

var someObject = {
    text: 'Hello from callback',
    doItWhenReady: function () {
        alert(this.text);
    }
}

function process(callback) {
    if (typeof callback !== 'function') {
         callback = false;
    }

    if (callback) {
         callback();
    }
}

process(someObject.doItWhenReady);

Interesujące są tutaj pierwsze linie, w których deklarujemy obiekt „someObject”. Jak widać mamy tam zdefiniowaną metodę „doItWhenReady”, która odnosi się do właściwości „text” tegoż obiektu. Dalej, identyczny z poprzednim kod funkcji „process”, a na końcu wywołanie tej funkcji – widać tutaj, że przekazujemy do niej referencję do metody „doItWhenReady” obiektu „someObject”.

Rozwiązanie problemu

Jak się pewnie domyślacie, powyższy kod nie zadziała jak należy. Wszystko oczywiście przez odwołanie „this.text” w metodzie „doItWhenReady”, które w tym momencie jest nie zdefiniowane – w przypadku wywołania zwrotnego, metoda wywoływana jest w kontekście funkcji „process”, a nie w kontekście obiektu… Rozwiązaniem jest przekazanie do funkcji obiektu, w kontekście którego ma być wywoływana funkcja callback:

function process(callback, obj) {
    if (typeof callback !== 'function') {
         callback = false;
    }

    if (callback) {
         callback.call(obj, null);
    }
}

process(someObject.doItWhenReady, someObject);

Deklaracja funkcji „process” została rozszerzona o nowy parametr „obj” – posłuży nam on do przekazania do funkcji referencji obiektu, w kontekście którego będziemy wywoływać funkcję callback. W zaznaczonej linii widać jak został on wykorzystany – zamiast zwykłego wywołania funkcji „callback” dokonujemy aplikacji funkcji za pomocą metody „Function.prototype.call” (oproszczona wersja metody „Function.prototype.apply”, która przyjmuje tylko jeden parametr), który jest tak naprawdę innym, bardziej zaawansowanym i dającym większe możliwości sposobem wywoływania funkcji w języku JavaScript.

W ostatniej linii widzimy zmienione wywołanie funkcji „process” – jako drugi parametr przekazujemy po prostu obiekt „someObject”. Dla lubiących sprawdzać wszystko samemu, tutaj jsfiddle z powyższym przykładem.

Podsumowanie

Samo zagadnienie aplikacji funkcji to temat co najmniej na osobny wpis, tak więc nie będę go tutaj szczegółowo rozwijał. Moją intencją było pokazanie tylko sposobu rozwiązania problemu przekazywania metody obiektu jako callback. Być może rozwinę ten właśnie temat w przyszłości, zachęcam jednak do samodzielnego jego zgłębienia 🙂

CHCESZ DARMOWEGO E-BOOKA?

Jeśli chcesz otrzymać mojego e-booka: Rozmowa Kwalifikacyjna - pytania z podstaw JavaScript zostaw mi swój e-mail:

Oprócz tego co poniedziałek dostaniesz maila z listą moich wpisów z poprzedniego tygodnia!

  • Grzegorz

    To jest kiepski pomysł żeby funkcję specjalnie rozszerzać o drugi parametr.

    Lepiej wykorzystać to co się ma i zrobić po prostu :

    process(function(){
    someObject.doItWhenReady();
    });

    Mamy pełną kontrolę nad kodem i nie jesteśmy uzależnieni od implementacji wewnętrznej biblioteki.

    Ewentualnie można skorzystać z bind :
    process(someObject.doItWhenReady.bind(someObject))

    Najgorsze co możemy zrobić to poszerzać listę parametrów. Powinno zawsze dążyć się żeby zestaw parametrów danej funkcji był jak najmniejszy, oraz parametry względem siebie powinny być ortogonalne.

    • Burczu

      Hej, dzięki za komentarz. Pierwszy zaproponowany przez Ciebie sposób wygląda trochę „hakersko” – wymaga dodatkowego opakowania funkcji w funkcję. Dużo lepsze jest moim zdaniem drugie Twoje rozwiązanie…
      Nie upieram się, że moje rozwiązanie jest najlepsze z możliwoch – cieszę się, że wywołało dyskusje i inne propozycje rozwiązania tego problemu 🙂

  • A nie lepiej wewnątrz obiektu zrobić var that = this; i odwoływać się do that? Czy w tej wersji funkcji process da się wykorzystać jako callback funkcję nie należącą do żadnego obiektu?

    • Burczu

      To też jest całkiem dobre rozwiązanie – zresztą uważam, że dobrą praktyką jest takie przypisywanie „this” do „that”. Eliminuje to wszelkie niejasności dotyczące „this” w danym kontekście…

    • Sebastian T.

      @Piotr Perak,

      masz na myśli coś takiego? http://jsfiddle.net/69wH5/2/
      To nie działa. that jest dalej aliasem dla this w takim samym kontekście, w jakim nie chcemy, aby było.

  • Piotr F.

    @Sebastian T.

    Unikamy zmiany kontekstu.

    http://jsfiddle.net/69wH5/6/