Domyślne wartości parametrów funkcji

Funkcje stanowią jeden z najważniejszych elementów języka JavaScript. Problem jednak zaczyna się, gdy chcemy wskazać domyślą wartość wszystkich lub tylko wybranych parametrów przekazywanych do funkcji.

Załóżmy, że chcemy napisać funkcję add, która będzie pobierała trzy argumenty (a, b,c) i zwróci sumę a+b+c. Chcemy jednak dać użytkownikowi możliwość wpisania jednego, dwóch lub trzech argumentów w wywołaniu funkcji, a dla brakujących parametrów przypiszemy wartości domyślne.

W wielu językach programowania problem ten można łatwo rozwiązać stosując zapis podobny do poniższego:

function add(a=1,b=1,c=1){
   return a + b + c;
}

add(); //3 (1+1+1)
add(2,2); //5 (1+2+2)
add(2,3,4); //9 (2+3+4)

Problem jednak w tym, że powyższy kod wymaga przeglądarki zgodnej ze standardem ECMAScript 6. W starszych przeglądarkach kod nie zadziała prawidłowo, gdyż nie obsługują one bezpośredniego przypisywania wartości domyślnych przy deklarowaniu parametrów funkcji.

Można pokusić się o stosowanie specjalnych narzędzi do transpilacji kodu na kod zgodny z ECMAScript 5 lub 3, albo użyć innego rozwiązania, bazującego na sprawdzaniu argumentów funkcji.

Domyślne wartości ustalane operatorem OR

W wielu poradnikach można znaleźć zapisy podobne do poniższego kodu:

function add(a,b,c){
  var a = a || 1, 
      b = b || 1,
      c = c || 1;
  return a + b + c;
}

add(); //3; (1+1+1)
add(2,2); //5 (2+2+1)
add(3,3,3); //9 (3+3+3)

Kod teoretycznie działa, ale pytanie czy można w jakiś sposób podać wartość b i c pomijając wartość a, czyli korzystając dla niej z wartości domyślnej?

Otóż tak – w takiej sytuacji musimy wywołać naszą funkcję np. w sposób pokazany poniżej:

add(null, 2, 2); //5 (1+2+2)
add(null, null, 5); //7 (1+1+5)
add(undefined, undefined, 5); //7 (1+1+5)
add(0,0,2); //4 ups... 0+0+2 === 4?

Hmm, chyba nie do końca nasza funkcja działa prawidłowo. Przeanalizujmy zatem jak dokładnie działa operator OR ( ” || „).

Wewnątrz naszej funkcji stosujemy zapis a = a || 1, chcąc nadać domyślą wartość a równą 1 w przypadku nie podania tego argumentu. W JavaScript nie możemy jednak pomijać argumentów przekazywanych w wywołaniu funkcji. Możemy ich podać mniej lub więcej niż funkcja oczekuje, ale zawsze muszą być one podawane w kolejności takiej, jaka została przyjęta w czasie deklarowania funkcji (dlatego przy większej liczbie parametrów lepszym rozwiązaniem jest przekazywanie obiektu, ale to temat do dyskusji przy omawianiu wzorców projektowych). Z tego względu aby pominąć dwa początkowe argumenty (a i b) musimy w ich miejsce przekazać specjalny typ null lub undefined i dopiero na trzeciej pozycji możemy podać wartość argumentu „c”.

Problem jednak pojawił się, gdy chcieliśmy wywołać funkcję z argumentem a i b równym zero. Spodziewaliśmy się otrzymać w wyniku „2” jako sumę 0+0+2, ale tak się nie stało…

Czy operator OR to na pewno dobre rozwiązanie…?

Otóż operator OR działa w taki sposób, że w pierwszej kolejności konwertuje wartość parametru a (pierwszego argumentu w wywołaniu funkcji) do wartości typu boolean (true/false). Jeśli po lewej stronie operatora ” || ” wystąpi element, który przekonwertuje się na true to właśnie on zostanie zwrócony i przypisany do zmiennej „a”.

Musimy jednak przypomnieć sobie jakie wartości są w JavaScript konwertowane na fałsz. W standardzie ECMAScript założono, że do wartości typu boolean false konwertują się: false, null, undefined, NaN, ” (pusty ciąg znakowy), 0 (zero, a dokładniej mówiąc -0 i +0). Wszystko po za wymienionymi sześcioma wartościami zostanie zamieniona na wartość true.

Co się zatem stało w naszym wywołaniu add(0,0,2)? Dwie początkowe wartości, czyli argumenty a i b otrzymały wartość zero, którą operator OR potraktuje jako fałsz, a więc przypisze do zmiennych wartości wskazane jako domyślne (prawa strona operatora OR).

Sprawdzenie typu przekazanej wartości

Aby zabezpieczyć się przed tego typu problemami można zastosować nieco dłuższą, ale bardziej precyzyjną kontrolę przekazywanych parametrów:

function add(a,b,c){
  var a = (typeof a !== 'undefined') ? a : 1,
      b = (typeof b !== 'undefined') ? b : 1,
      c = (typeof c !== 'undefined') ? c: 1;
  return a + b + c;
}

add(2,2,2); //6
add(); //3
add(0,0,2); //2 - teraz jest ok
add(undefined,2,2); //1 (1+2+2)
add(null, 2, 2); //4 (0+2+2)

No dobrze, udało nam się rozwiązać problem przekazywania parametrów o wartości równej zero. Jeśli jawnie pominiemy pierwszy argument, przypisując mu wartość undefined to również uzyskamy oczekiwany wynik, gdyż do zmiennej „a” zostanie przypisana wartość domyślna, czyli jedność.

Mamy jednak kolejny problem, gdyż przy wskazaniu pierwszego argumentu jako null został on potraktowany jako argument poprawny o wartości zero. Skąd się to wzięło? Otóż z bardzo prostej przyczyny: w naszym kodzie testujemy wyłącznie typ undefined, czyli wszystko inne (a więc i null) stanowią dla nas poprawną zmienną, dla której nie mamy stosować wartości domyślnej. W naszym wypadku, przy podaniu parametru a o wartości null spełniliśmy warunek określony w pierwszym członie wyrażenia trójkowego „(warunek) ? (true) : (false) „.

Problemem jednak jest tutaj zachowanie się operatora „+”, który może działać w różny sposób w zależności od sytuacji. W naszym przypadku nie mamy żadnej zmiennej (a, b lub c) będącej typem string, a więc operator „+” będzie chciał wykonać matematyczną operację dodawania. No dobrze, ale jak dodać: null + 2 + 2? W tej sytuacji silnik JavaScript dokona niejawnej konwersji typu null na wartość liczbową co spowoduje zamienienie null na wartość zero i w efekcie wykonanie operacji 0+2+2.

Czy warto zatem testować również typ null?

Rodzi się zatem pytanie czy może warto byłoby również testować wartość null, dzięki czemu uzyskamy zawsze oczekiwane rezultaty dodawania?

Niektórzy pewnie zgodziliby się z tym stwierdzeniem, lecz ja uważam że w celu jawnego pominięcia parametrów w wywołaniu funkcji należy dopuścić wyłącznie podanie tego parametru jako undefined.

Być może powiesz, że jestem mało elastyczny i nie chcę tworzyć kodu bardziej uniwersalnego. Uważam jednak, że jest wręcz odwrotnie. Otóż może zdarzyć się sytuacja, gdy będziemy napisali funkcję, która pobiera jakieś argumenty, a następnie część z nich lub wszystkie próbuje zapisać w bazie danych (np. poprzez technologię Ajax).

Typ wartości określany jako undefined jest charakterystyczny dla języka JavaScript i nie występuje w systemach bazo-dachowych, a więc na pewno nie należy dopuszczać do zapisu takiej wartości w bazie (która tak na prawdę musiałaby w tym momencie zostać zapisana jako ciąg znakowy – np. char, varchar itp.). Z kolei wartość typu null jest jak najbardziej prawidłową wartością dla pola bazodanowego i może zostać zapisana w bazie (nie w formie string jak miałoby to miejsce z wartością undefined lecz jako osobny typ, czyli wartość NULL).

Jeśli zatem dodamy do naszej funkcji testowanie również pod kątem wartości null to owszem, zyskamy możliwość pomijania argumentów na dwa sposoby (undefined lub null) lecz musimy wtedy mieć pewność, że zmienne te nie będą zapisywane w bazie danych i nie będziemy potrzebowali przypisania im faktycznie wartości null (co może być czasem przydatne nie tylko w bazie lecz również w dalszych operacjach na zmiennych w JavaScript).

No dobrze, ale czy musimy co chwilę powtarzać instrukcję typeof?

Już na pierwszy rzut oka widać, że w naszej funkcji add musieliśmy aż trzykrotnie pisać kod sprawdzający zmienną i przypisujący jej wartość domyślą. A co jeśli mam do napisania dwadzieścia różnych funkcji i każda z nich przyjmuje jakieś parametry, dla których chcę ustawiać wartości domyślne? Czy można to jakoś usprawnić?

Można na przykład napisać własną funkcję pomocniczą w formie:

function check(param, defValue){
  return (typeof param !== 'undefined') ? param : defValue;
}

Następnie możemy korzystać z niej przy tworzeniu kolejnych funkcji w projekcie:

function add(a,b,c){
   return check(a,1) + check(b,1) + check(c,1);
}

add(2,2,2); //6
add(0,0,2); //2
add(undefined,2,2); //5
add(null,2,2); //4

Możesz zapytać dlaczego wartości domyślnej „1” nie przypisałem w funkcji check. Otóż z prostej przyczyny – jedynka to domyślna wartość parametrów w funkcji add, ale czy masz pewność, że taka samą wartość domyślną zastosujesz w innych swoich funkcjach? A może w innej funkcji będziesz chciał przypisać w ogóle nie wartość liczbową (jedynka) lecz ciąg znakowy? Staraj się tworzyć swoje funkcje tak, aby były możliwie uniwersalne.

Jeśli jednak w swoim projekcie masz wiele funkcji wymagających wartości domyślnych to polecam stosowanie standardu ECMAScript 6 z przypisywaniem default value już w momencie deklaracji funkcji i określania jej argumentów:

function add(a=1, b=1){ return a + b; }

add(2,2); //4 (2+2)
add(0,2); //2 (0+2)
add(null,2); //2 (0+2)
add(undefined,2); //3 (1+2)

Jak widać działanie domyślnych parametrów w standardzie ECMAScript 6 jest zgodne z proponowaną przeze mnie konwencją aby do pomijania argumentów stosować wyłącznie undefined, natomiast null traktować jako wartość poprawną (czyli przekazaną, a nie pominiętą).

Pamiętajmy jednak, że w tym wypadku może zajść konieczność zastosowania dodatkowego narzędzia, tzw. transpilatora kodu JavaScript. Domyślnych parametrów funkcji nie da się zapewnić poprzez tradycyjny polyfill gdyż jest to część składni języka. Praktycznie każdy z ważniejszych transpilatorów bez problemu radzi sobie z przepisaniem parametrów domyślnych ES6 na zgodność ze standardem ES3 i ES5.