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);
:
- Znajduje obiekt
worker
- Znajduje funkcję
speak
w obiekcieworker
- Czytając kod dalej, zamiast nawiasów, które wywołałyby funkcję
speak
, znajduje metodęcall
- Natrafia na pierwszy argument metody
call
- jest nią nowy kontekst wywołania funkcjispeak
(obiektdeveloper
) - Wywołuje funkcję
speak
w konktekście obiektudeveloper
- W związku z tym, że zmienna
work
w obiekciedeveloper
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);
:
- Znajduje obiekt
hacker
- Znajduje w nim metodę
showAge
- Czyta dalej i natrafia na kropkę zamiast nawiasów, więc nie wywołuje metody
showAge
, ale czyta dalej - Natrafia na wywołanie metody
call
- Natrafia na pierwszy argument metody
call
- jest nią nowy kontekst wywołania metodyshowAge
(w tym wypadku obiektfemale
) - Natrafia na drugi argument - jest nią parametr przekazywany do funkcji
showAge
(female.name
) - Wywołuje metodę
showAge
w kontekście obiektu female i z parametrem przekazanym jako argument do funkcjishowAge
(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