Wywołanie funkcji metodą call i apply

Zdarzają się sytuacje, gdy nie możemy bezpośrednio wywołać jakieś funkcji ze względu na konieczność zapewnienia odpowiedniego wskaźnika this lub przekazania innego rodzaju argumentów niż funkcja oczekuje. W takich przypadkach z pomocą przychodzą metody call i apply.

Problem wywoływania funkcji zaczniemy od konkretnego przykładu. Mamy zadeklarowaną tablicę arr, składającą się z samych liczb całkowitych. Naszym zadaniem jest znalezienie maksymalnej wartości ze wszystkich elementów tablicy.

Możemy do tego problemu podejść na kilka sposobów. Jednym z nich jest posortowanie elementów tablicy w kolejności rosnącej lub malejącej i odczytanie odpowiednio ostatniego lub pierwszego elementu:

var arr = [1,2,5,3,4,7,2];
function maxValue(array) {
    return array.sort((a,b) => b-a)[0];
}

maxValue(arr); //7

Zadanie to można jednak wykonać również bez tworzenia dodatkowej funkcji maxValue i uciekania się do sortowania tablicy z wykorzystaniem funkcji zwrotnej (callback na metodzie sort). Wykorzystamy w tym celu metodę  max() obiektu globalnego Math. Problem jednak w tym, że metoda max przyjmuje jako argumenty zmienne typu number i z pośród nich wybiera wartość największą. Nie możemy więc przekazać jej tablicy:

var arr = [1,2,5,3,4,7,2];
Math.max(arr); //NaN ups... coś poszło nie tak

//A teraz z wykorzystaniem metody apply
Math.max.apply(null, arr); //7 teraz jest ok!

No dobrze, widzimy zatem, że w pewnych przypadkach faktycznie potrzebna jest możliwość nieco innego wywołania funkcji niż sposób bezpośredni. Przeanalizujmy jednak co dokładnie robią funkcje call i apply.

Argumenty metod call i apply

Metody call i apply przyjmują co co najmniej jeden argument. Jeśli jest ich więcej to pierwszy odnosi się do wskaźnika this (omówimy to za chwilę), a drugi i kolejne argumenty to parametry, które zostaną przekazane jako argumenty przy wywołaniu funkcji docelowej, przy czym dla metody apply można podać nie więcej niż dwa argumenty (czyli this oraz tablicę wartości).

Przekazana tablica (w metodzie apply) zostanie rozpakowana na pojedyncze wartości i w takiej formie przekazana do funkcji docelowej. Dlatego właśnie w naszym zadaniu musieliśmy użyć metody apply, a nie call, gdyż przekazywana wartość to tablica arr. Wywołanie metody Math.max.apply(arr) spowodowało wewnętrzne rozłożenie tablicy na pojedyncze wartości i w rzeczywistości ww. zapis spowodował wywołanie:

var arr = [1,2,5,3,4,7,2];
Math.max(1,2,5,3,4,7,2); //7

Metoda call często jest stosowana w czasie pracy z wieloma argumentami funkcji. Dostęp do argumentów przekazanych do funkcji odbywa się za pośrednictwem obiektu arguments, który nie jest jednak pełnoprawną tablicą! Nie możemy więc wywołać na nim np. metody map czy forEach. W tym celu często stosowaną praktyką jest przypisanie argumentów do nowej zmiennej będącej tablicą:

function fn (a,b,c,d) {
    var arg = Array.prototype.slice.call(arguments);
    return arg[arg.length - 1]; //Ostatni element tablicy;
}

fn(1,2,3,4); //4
fn(1,2,3,4,5,6,7); //7 Mimo, że w deklaracji funkcji są tylko 4 argumenty!

Od tej chwili wewnątrz funkcji fn możemy operować na argumentach za pośrednictwem zmiennej arg, która jest pełnoprawną tablicą. W standardzie ECMA Script 6 wprowadzono nowy, lepszy sposób operowania na pseudotablicach (jak właśnie arguments) ale do tego powrócimy później.

Uważny czytelnik szybko zauważy, że w drugim przykładzie przekazano metodzie call tylko jeden parametr. Otóż jeśli nie przypiszemy jawnie dwóch parametrów (lub więcej w metodzie call) to będzie on określał this dla wywołania funkcji i w całości zostanie jej przekazana referencja do tego obiektu. W przypadku wcześniejszego zastosowania meto Math.max nie mogliśmy zastosować wywołania apply a jednym parametrem gdyż potrzebowaliśmy jawnego przekazania tablicy jako argumentu (czyli musieliśmy przekazać drugi element). W naszym przykładzie wskazaliśmy jawnie na pusty obiekt (null). Czasami w literaturze znajdują się również wywołania:

var arr = [1,2,5,3,4,7,2];
Math.max.apply(Math, arr); //7
Math.max.apply(null, arr); //7
Math.max.apply(arr); //-Infinity ups...

Wykorzystanie call do sprawdzenia typu zmiennej

Jednym z ciekawszych przykładów wykorzystania metody call jest sprawdzenie typu zmiennej z wykorzystaniem metody toString obiektu Object. Pytanie jednak po co w ogóle się tym zajmować, przecież istnieje prosta instrukcja typeof? Otóż z instrukcją tą związany jest pewien drobny problem:

var num = 1, 
    str = 'text', 
    obj = {},
    arr = [1,2,3], 
    reg = /\d/g;

typeof num; //'number'
typeof str; //'string'
typeof obj; //'object'
typeof arr; //'object' ups...
typeof reg; //'object' ups...

Jak widać instrukcja typeof nie potrafi precyzyjnie rozróżnić różnych obiektów,  a w JavaScript tablice oraz wyrażenia regularne są obiektami. Jak zatem sprawdzić czy arr to tablica, a reg to obiekt RegExp?

Object.prototype.toString.call(obj); //'[object Object]'
Object.prototype.toString.call(arr); //'[object Array]'
Object.prototype.toString.call(reg); //'[object RegExp]'

Metoda toString obiektu globalnego nie pobiera parametrów. W przypadku wywoływania jej na zmiennej typu number można przekazać argument, lecz będzie on oznaczał podstawę konwersji systemu liczbowego co umożliwia łatwe przekonwertowanie np. liczby z systemu dziesiętnego na binarny itp.

Metoda toString obiektu globalnego Object zwraca typ obiektu z jego konstruktorem (drugi człon w [object Constructor]). Aby jednak wywołać tę metodę dla konkretnej zmiennej musimy wykorzystać pośrednią metodę call.

Sposób ten sprawdzi się w wielu sytuacjach, lecz np. dla tablic istnieje krótsza metoda, bazująca na metodzie obiektu Array:

var a = [1,2,3],
    b = 'text';
Array.isArray(a); //true
Array.isArray(b); //false

Metody z obiektów globalnych (np. Array.isArray) są często dokładniejsze i szybsze niż uciekanie się do sztuczek z call lub apply. Warto zatem śledzić rozwój standardu ECMA Script gdyż kwestia kontroli typu zmiennych jest dość mocno rozwijana.

Jawne wskazywanie this w wywołaniu call / apply

Powróćmy jeszcze na chwilę do pierwszego, opcjonalnego argumentu w wywołaniu metod call i apply. Określa on wskaźnik this, czyli obiekt, który będzie traktowany jako this w wywołaniu funkcji docelowej.

Rozważmy poniższy kod:

function myName () {
    return 'My name is ' + this.name;
}
var firstPerson = {name: 'John'},
    secondPerson = {name: 'Charlie'}

myName.call(firstPerson); //'My name is John'
myName.call(secondPerson); //'My name is Charlie'

W powyższym przykładzie wywołujemy funkcję myName wskazując jako obiekt this kolejne obiekty firstPerson i secondPerson. Nie ma potrzeby przekazywania drugiego argumentu w wywołaniu metody call gdyż funkcja docelowa myName nie oczekuje i nie wykorzystuje żadnych argumentów, a jedynie właściwość name z obiektu wskazanego jako this.

Metody call i apply są w wielu przypadkach przydatne, lecz należy stosować je z rozwagą i zawsze dobrze przeanalizować czy chcemy przekazać dwa lub więcej czy jeden argument ze wskazaniem konkretnego obiektu pełniącego rolę wskaźnika this.

Wracając do naszego pierwszego zadania warto również wspomnieć o jeszcze krótszej formie zapisu metody max zgodnej ze standardem ECMA Script 6:

var arr = [1,5,3,4,8,9,7,10];
Math.max(...arr); //10

Rozwiązanie to wykorzystuje nowy operator ” … „, którego zadaniem jest w tym wypadku rozpakowanie (rozproszenie) tablicy arr na pojedyncze wartości i przekazanie ich jako argumenty dla funkcji max. Zapis ten jest najkrótszym rozwiązaniem lecz przed jego zastosowaniem należy zastanowić się w jakim środowisku będzie uruchamiana aplikacja i w razie potrzeby użyć dodatkowych transpilatorów kodu, zmieniających kod ECMA Script 6 na kod zgodny ze standardem ES 5 lub ES 3.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *