Problemy związane z niejawną konwersją do true/false

Niejawna konwersja typów do wartości Boolean występuje praktycznie w każdym kodzie JavaScript, np. gdy badamy istnienie zmiennej w instrukcjach warunkowych. Warto jednak wiedzieć o kilku niuansach związanych z konwersją typów w języku JavaScript.

Często spotykaną sytuacją jest korzystanie z instrukcji warunkowych if do sprawdzenia teoretycznie prostych warunków, np:

var a = 1;
if (a) { 
   //instrukcje gdy istnieje zmienna a;
}

W takiej sytuacji następuje niejawna konwersja zmiennej a (wewnątrz instrukcji warunkowej if) na wartość typu boolean czyli true lub false, gdyż takiej wartości oczekuje instrukcja if. Zapis taki jest krótki i teoretycznie oczywisty. Zwróćmy jednak uwagę na kilka drobnych problemów spotykanych w praktyce.

Po pierwsze – czym w JavaScript jest wartość false?

Na początek musimy przypomnieć sobie czym właściwie w JavaScript jest wartość false?

Otóż wbrew wielu opiniom zasady konwersji typów w JavaScript są bardzo proste. Na wartość false zostaną przekonwertowane tylko i wyłącznie następujące wartości:

  • null,
  • undefined (jawne lub niejawne),
  • ” oraz „” (puste ciągi znakowe),
  • 0 (zero, a dokładniej mówiąc +0 i -0),
  • NaN (Not a Number)
  • oraz wartość false (po za jednym drobnym wyjątkiem…).

Wszystkie pozostałe wartości zostaną przekonwertowane do wartości true. Teoretycznie wydaje się to proste i czytelne, lecz w praktyce konwersja typów może być powodem długich poszukiwań błędów logicznych w kodzie JavaScript.

Testowanie konwersji typów na instrukcjach warunkowych wymagałoby dość rozbudowanych kodów, dlatego w artykule będziemy posługiwali się formą zapisu jawnej konwersji typu zmiennej:

var a = 5;
!a; //false
!!a; //true

Jeden znak wykrzyknika powoduje  konwersję do wartości boolean z jednoczesną negacją. W naszym wypadku zmienna a ma przypisaną wartość, która powinna przekonwertować się do true. Z tego powodu aby jawnie przekonwertować wartość na true/false musimy użyć dwóch wykrzykników, gdzie drugi wykrzyknik spowoduje ponowne odwrócenie wartości.

Zacznijmy od przeszukiwania ciągów znakowych

Do sprawdzania czy w zmiennej typu string znajdują się określone ciągi znakowe najczęściej stosowane są metody indexOf, lastInfexOf oraz metoda search. W przypadku bardziej złożonych wzorców stosuje się wyrażenia regularne.

Spójrzmy na poniższy kod:

var str = 'Przykładowy tekst abcd';
!!(str.indexOf('abcd')); //true
!!(str.indexOf('123')); //true - ups...
!!(str.indexOf('Przykładowy')); //false - ups...

!!(str.lastIndexOf('123')); //true - ups...

!!(str.search(/[0-9]/g)); //true - ups...

Jak widać wyniki nie są takie, jakich moglibyśmy oczekiwać. W pierwszym wywołaniu metody indexOf otrzymaliśmy true gdyż faktycznie ciąg „abcd” stanowi część wartości zmiennej str.

Ale co z drugim i trzecim przypadkiem? Otóż w drugim wywołaniu metody indexOf zapomnieliśmy, że w przypadku niepowodzenia, czyli nie znalezienia szukanego ciągu zwróci ona wartość -1, a więc wartość która zostanie przekonwertowana na true. Z wartości liczbowych jedynie zero zostałoby zamienione na false i właśnie taki przypadek wystąpił w trzecim wywołaniu indexOf gdy metoda zwróciła zerowy indeks wystąpienia dopasowania, gdyż szukany ciąg znalazła na początku wartości zmiennej str.

Z tych względów stosując w instrukcjach warunkowych metody indexOf, lastIndexOf oraz search zawsze przyrównuj zwracaną przez nie wartość do wartości -1, zero lub z wykorzystaniem bitowego operatora NOT.

var str = 'Przykładowy tekst abcd';

str.indexOf('abcd') !== -1; //true
str.indexOf('123') !== -1; //false
str.indexOf('Przykładowy') !== -1; //true
str.indexOf('przykładowy') !== -1; //false - wielkość liter!

!!~str.indexOf('123'); //false
!!~str.indexOf('Przykładowy'); //true
!!~str.indexOf('abcd'); //true

Takie same zasady obowiązują również dla metody lastIndexOf oraz dla metody search (która jako argument oczekuje nie ciągu typu String lecz wyrażenia regularnego RegExp). Metodę wykorzystującą bitowy operator NOT dokładnie omawiam we wpisie poświęconym przeszukiwaniu ciągów znakowych.

A teraz coś z obsługi DOM

Korzystając z modelu DOM praktycznie zawsze zachodzi konieczność uzyskania referencji do wybranych elementów drzewa HTML. Obecnie najczęściej stosuje się do tego celu metody obiektu document takie jak getElementById, getElementsByTagName, getElementsByClassName, querySelector, querySelectorAll.

Problem jednak pojawia się, gdy operujemy na elementach, które mogą być tworzone dynamicznie poprzez inne skrypty JS. W tym momencie zachodzi konieczność wcześniejszego sprawdzenia czy udało się pobrać jakieś referencje. Jednym z rozwiązań jest użycie instrukcji warunkowej if:

var elementArray = document.querySelectorAll('.jakas_klasa'),
    element = document.getElementById('jakies_id');

if (elementArray) {
    //operacje jeśli istnieją referencje do .jakas_klasa
}
if (element) {
    //operacje jeśli istnieje referencja do #jakies_id
}

Teoretycznie kod wygląda na poprawny, gdyż zmienne przekazane do instrukcji warunkowej powinny niejawnie przekonwertować się do true/false. Załóżmy jednak, że nie istnieją żadne elementy DOM z przypisaną klasą „jakas_klasa” lub z id „jakies_id”. Oczekujemy zatem, że obie zmienne będą przekonwertowane do false.

!!elementArray; //true - ups...
!!element; //false

elementArray; //[]
element; //null

Widzimy jednak, że tylko zmienna „element” zostanie przekonwertowana do false, natomiast zmienna elementArray będzie traktowana jako wartość true. Otóż stało się tak dlatego, że metody takie jak querySelectorAll, getElementsByTagName czy getElementsByClassName zawsze zwracają tzw. pseudotablicę (z referencjami HTMLElement), a jeśli nie znajdą żadnych elementów spełniających podane kryteria to zwrócą pustą tablicę.

Jak wiemy z wcześniejszej części artykułu w języku JavaScript do wartości false konwertuje się tylko sześć wybranych wartości, ale nie ma wśród nich tablicy – zarówno pustej jak i zawierającej jakieś elementy. Każda tablica w JavaScript jest obiektem, a obiekty zawsze konwertowane są do true. W naszym wypadku wspomniałem jednak, że jest to tzw. pseudotablica:

var a = document.querySelectorAll('.nie_istniejaca_klasa'),
    b = [];

a; //[]
b; //[]

typeof a; //'object'
typeof b; //'object'

Object.prototype.toString.apply(a); //'[object NodeList]'
Object.prototype.toString.apply(b); //'[object Array]'

Dopiero kontrola z użyciem metody toString obiektu globalnego Object ukazuje różnice pomiędzy tablicami a i b. Pseudotablica zawarta w zmiennej a nie posiada metod i właściwości odziedziczonych prototypowo po obiekcie Array (choć posiada właściwość length). Istnieją pewne sztuczki zmieniające ją w pełnoprawną tablicę, np. z wykorzystaniem metody slice (lub nowy operator ze standardu ECMA Script 6), co pozwala korzystać z metod tablicowych jak map, forEach, filter itp.

Należy o tym pamiętać jeśli chcemy uzależnić instrukcje od tego, czy udało nam się uzyskać referencje do jakiś elementów DOM. W naszym przykładzie można by np. testować czy (elementA.lenth > 0), co będzie spełnione tylko wtedy, gdy tablica będzie posiadała co najmniej jeden element ([].length === 0). Można to również uprościć badając tylko warunek if(elementA.length) gdyż jeśli uzyska on wartość zerową to automatycznie zostanie przekonwertowany do false, zgodnie z oczekiwaniami.

W przypadku metod zwracających pojedynczą referencję nie ma takich problemów, gdyż w razie niepowodzenia zwracają one wartość null, która zgodnie z zasadami JS zostanie przekonwertowana do false.

Wartości domyślne parametrów funkcji

W wielu poradnikach można znaleźć informacje, że język JavaScript nie obsługuje możliwości przypisywania domyślnych wartości dla parametrów funkcji i można w tym celu stosować operator alternatywy OR. Jest to jednak bardzo zła praktyka, która w pewnych przypadkach prowadzi do poważnych błędów logicznych w kodzie.

Spójrzmy na poniższy przykład:

function multi(a,b) {
    var a = a || 1,
        b = b || 1;
    return a * b;
}

multi(2,2); //4
multi(1,2); //2
multi(0,1); //1 ups...

Problem wystąpił gdy chcieliśmy uzyskać wynik mnożenia zera i jedynki, a otrzymaliśmy w wyniku „1”. Stało się tak dlatego, że operator OR dokonuje konwersji typu zmiennej znajdującej się z jego lewej strony na true/false i jeśli w wyniku uzyska false, to zwraca wartość z prawej strony operatora „||”. Jak wiemy zero w JavaScript konwertowane jest do false, dlatego w ostatnim wywołaniu funkcja użyła domyślnej wartości „1” dla zmiennej a.

function multi(a,b) {
    var a = (typeof a !== 'undefined') ? a : 1,
        b = (typeof b !== 'undefined') ? b : 1;
    return a * b;
}

multi(2,2); //4
multi(1,2); //2
multi(0,1); //0 teraz jest ok

Aby uniknąć problemów z konwersją typów do false użyliśmy jawnego testowania, czy przekazana zmienna ma wartość undefined i jeśli tak, to przypisujemy jej wartość domyślną „1”. Aby jawnie pominąć dany parametr w wywołaniu funkcji musimy w tym wypadku przypisać mu wartość undefined.

Więcej na ten temat napisałem w artykule Domyślne wartości parametrów funkcji, wraz z omówieniem metody zgodnej ze standardem ECMA Script 6.

Czy false może zostać niejawnie przekonwertowane na true?

Wielu czytelników zapewne zastanawia się w tym miejscu jak w ogóle można dyskutować na temat tego czy false === true, przecież to wbrew samej idei wartości boolean.

Przeanalizujmy jednak uważnie poniższy kod:

var a = false,
    b = Boolean(false),
    c = new Boolean(false);

!!a; //false
!!b; //false
!!c; //true - ups... jak to?

typeof a; //'boolean'
typeof b; //'boolean'
typeof c; //'object'
c.toString(); //'false'

Zmiennej a w sposób bezpośredni przypisaliśmy wartość false. Wartość zmiennej b stworzyliśmy z wykorzystaniem konstruktora Boolean, co również owocuje zwróceniem wartości false i zmiennej typu boolean.

Ale co się stało w momencie deklaracji zmiennej c? Otóż w tym wypadku zastosowanie słowa new spowodowało stworzenie nowego obiektu z tzw. wartością prymitywną równą ‚false‚. Na wstępie wymieniłem wartości, które w JavaScript konwertowane są do fałszu. Zwróć uwagę, że nie ma tam obiektów (nawet pustych), a więc każdy obiekt konwertowany jest na true. Z tego powodu zmienna c została przekonwertowana na wartość true gdyż jest jawnie zadeklarowanym obiektem!

Z tego powodu nigdy nie należy deklarować zmiennych typu boolean z wykorzystaniem operatora new Boolean gdyż w praktyce zmienna taka będzie później całkowicie nieprzydatna. Zmienne boolean najlepiej zawsze deklarować poprzez bezpośrednie przypisanie wartości true / false.

Dodaj komentarz

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