Metody call() i apply(), choć z początku mogą wydać się nieco dziwaczne, a ich użycie nieintuicyjne, potrafią znacznie uprościć pisanie kodu i poszerzyć wachlarz możliwości front end developera. Jeśli masz problem z ich zrozumieniem, bądź dotąd o nich nie słyszałeś, dobrze trafiłeś - po lekturze tego artykułu nie powinieneś mieć żadnych wątpliwości co do ich przydatności i łatwości użycia.

Podstawy teoretyczne

Jak zapewne wiesz, ponieważ w JavaScript funkcje są obiektami, mogą mieć one swoje własne funkcje, zwane metodami. Na przykład: 

function Developer () {
    this.work = "coding apps";
    this.speak = function () {
        console.log("My job is " + this.work);
    }
}
var me = new Developer();
me.speak(); //My job is coding apps

To, z czego niektórzy mogą nie zdawać sobie sprawy, to fakt, iż w JavaScript każda funkcja posiada cały zestaw metod już wbudowanych, gotowych do użycia. Są to między innymi interesujące nas dzisiaj:

  • Function.prototype.apply()
  • Function.prototype.call()

Każda funkcja ma także dwa powiązane z nią elementy:

  • Kontekst - czyli to, do czego odnosimy się za pomocą słowa kluczowego this
  • Argumenty - lista argumentów przekazanych do funkcji w momencie jej wywołania

Mając w pamięci te podstawy, możemy przejść do konkretów.

Metoda call()

Definicja

W najprostszy możliwy sposób metodę call() można opisać tymi słowami:

Metoda call() wywołuje funkcję z zadanym kontekstem i argumentami.

OK, prościzna. Ale co to oznacza w praktyce? Jak to wykorzystać?

Przykład 1: wywołanie funkcji w tym samym kontekście

Aby zrozumieć, do czego może nam się przydać metoda call(), weźmy najprostszą funkcję:

function add (a, b) {
    return a + b;
}
console.log(add(1, 2)); //3

Metoda call() daje nam dodatkowy sposób wywołania tej funkcji - prowadzący do uzyskania tego samego rezultatu:

console.log(add.call(this, 1, 2)); //3

Jako pierwszy argument podaliśmy kontekst, a następnie właściwe argumenty metody. W tym wywołaniu kontekstem jest this - jest więc to ten sam kontekst, który zostaje użyty przy standardowym wywołaniu funkcji.  Dlatego też wynik działania w obu przypadkach jest identyczny. Mamy więc funkcję wywołaną z tym samym kontekstem i tymi samymi argumentami, a więc i jej rezultat musi być ten sam.

Oczywiście takie użycie metody call() nie ma ma większego sensu w praktyce, gdyż wystarczy wywołać funkcję w standardowy sposób, by osiągnąć ten sam efekt. Istotą stosowania metody call() jest więc użycie innego kontekstu

Przykład 2: Wywołanie funkcji ze zmienionym kontekstem

var worker = {
    work: "copy-pasting",
    speak: function() {
        console.log("My job is " + this.work);
    }
};
var developer = {
    work: "coding apps"
};
worker.speak(); //My job is copy-pasting
developer.speak(); //Błąd - Obiekt developer nie ma metody speak
worker.speak.call(developer); //My job is coding apps

Co dzieje się w powyższym kodzie? Mimo, iż obiekt developer nie posiada własnej funkcji speak, udało nam się z niej skorzystać. Dokonaliśmy tego "pożyczając" ją od obiektu worker poprzez metodę call(). Pozostało jedynie poinformować ją, z którego obiektu ma odczytać zmienne. Robimy to podając, jako jej pierwszy parametr, odpowiedni kontekst. W tym wypadku kontekstem jest obiekt, z którego zmienne chcemy odczytać, czyli developer.

Oto, co w uproszczeniu wykonuje parser, gdy natrafia na linijkę worker.speak.call(developer); :

  1. Znajduje obiekt worker
  2. Znajduje funkcję speak w obiekcie worker
  3. Czytając kod dalej, zamiast nawiasów, które wywołałyby funkcję speak, znajduje metodę call
  4. Natrafia na pierwszy argument metody call - jest nią nowy kontekst wywołania funkcji speak (obiekt developer)
  5. Wywołuje funkcję speak w konktekście obiektu developer
  6. W związku z tym, że zmienna work w obiekcie developer zawiera string "coding apps", a nie "copy-pasting", rezultatem jest wypisanie do konsoli "My job is coding apps".

To tyle. Łatwo, prosto i przyjemnie.

Przykład 3: wywołanie funkcji, gdy oryginalny kontekst nie posiada wymaganych zmiennych

Żeby przekonać się, czy dobrze rozumiesz zasadę działania metody call(), zastanów się jaki będzie wynik działania poniższego kodu.

var hacker = {
    showAge: function(name) {
        console.log(name + " is " + this.age + " years old");
    }
};
var male = {
    age: 29,
    name: "Mirek"
};
var female = {
    age: 18,
    name: "Karyna"
};
hacker.showAge.call(male, male.name);
hacker.showAge.call(female, female.name);

Dla pewności, przeanalizujmy, co się dzieje, gdy parser natrafia na wywołanie hacker.showAge.call(female, female.name);:

  1. Znajduje obiekt hacker
  2. Znajduje w nim metodę showAge
  3. Czyta dalej i natrafia na kropkę zamiast nawiasów, więc nie wywołuje metody showAge, ale czyta dalej
  4. Natrafia na wywołanie metody call
  5. Natrafia na pierwszy argument metody call - jest nią nowy kontekst wywołania metody showAge (w tym wypadku obiekt female)
  6. Natrafia na drugi argument - jest nią parametr przekazywany do funkcji showAge (female.name)
  7. Wywołuje metodę showAge w kontekście obiektu female i z parametrem przekazanym jako argument do funkcji showAge (female.name)

Jak więc się zapewne domyślasz, wynikiem działania tego kodu jest wypisanie w konsoli linijek "Mirek is 29 years old" oraz  "Karyna is 18 years old".

Co bardzo istotne, mimo iż obiekt hacker nie posiada właściwości age, odwołanie się w metodzie showAge do this.age nie spowoduje błędu. Dzieje się tak dlatego, że każde wywołanie tej metody odbywa się w kontekście, który właściwość age posiada (zarówno obiekt male jak i female). Oczywiście, gdyby wywołać ją w kontekście obiektu nie posiadającego właściwości age (np. hacker.showName("Gilbert") ), skończyłoby się błędem.

Przykład 4: wywoływanie metod tablicowych na obiektach tablicopodobnych

Oczywiście można pisać kod w taki sposób, aby nie musieć uciekać się do pożyczania metod od innych obiektów. Są jednak momenty, gdy metoda call() staje się niezwykle przydatna i oszczędza pisania wielu linijek kodu. Mowa o wykorzystaniu metod tablicowych na obiektach, które tablicami tak naprawdę nie są.

Przykładowo kod, który usuwa ze stringa wszystkie cyfry mógłby wyglądać następująco:

var str = "9Jav4aSc3ri1pt je1st22 spo3ko06";
// wywołanie metody tablicowej w kontekście naszego stringa var noDigits = Array.prototype.filter.call(str, function(e) { return isNaN(parseInt(e)); //sprawdzenie, czy dany znak jest liczbą }).join(''); //zamiana tablicy na srtinga console.log(noDigits); //JavaScript jest spoko

Jeszcze ciekawszym zastosowaniem jest stworzenie funkcji, która zwraca posortowane argumenty do niej przekazane. Jak wiadomo obiekt arguments nie jest tablicą, a obiektem tablicopodobnym, więc wywołanie takiej funkcji nie zadziała:

function sortArgs () {
    return arguments.sort();
}
console.log(sortArgs(3,5,2,1,8,7)); //Błąd

Z pomocą przychodzi metoda call():

function sortArgs () {
    var args = Array.prototype.slice.call(arguments);
    return args.sort();
}
console.log(sortArgs(3,5,2,1,8,7)); //[ 1, 2, 3, 5, 7, 8 ]

Co można zapisać jeszcze prościej:

function sortArgs () {
    return [].slice.call(arguments).sort();
}
console.log(sortArgs(3,5,2,1,8,7)); //[ 1, 2, 3, 5, 7, 8 ]

Metoda Apply()

Metoda apply() różni się od call() tylko jednym małym szczegółem. Obok zadanego kontekstu przyjmuje ona wyłącznie jeden parametr zamiast ich listy. Parametrem tym jest tablica argumentów. W praktyce wygląda to tak:

function add (a, b, c) {
    return a + b + c;
}
console.log(add.call(this, 1, 2, 3)); //6
console.log(add.apply(this, [1, 2, 3])); //6