Dziwactwa JavaScriptu

Czasem wydaje mi się, że JavaScript przejął pałeczkę najbardziej hejtowanego języka od PHP. Krytycy powtarzają, że jest to język wyjątkowo nieprzemyślany, wręcz niechlujny, i dlatego nie można traktować go jako czegoś poważnego. Często jest obiektem żartów. Po Internecie krąży wiele snippetów, które dają nieoczekiwany wynik. I właśnie o takich kawałkach kodu będzie ten wpis, jednak zamiast się naśmiewać, sprawdzę dlaczego wynik jest taki, a nie inny.

I od razu uprzedzam - nie jest tak, że jestem javascriptowym wyjadaczem. Po prostu jest opcja sprawdzenia sobie pewnych rzeczy w standardzie, który jest ogólnie dostępny pod adresem: http://ecma-international.org/ecma-262/.




Cyferki

typeof NaN
// "number"


Not-a-Number
to number - wiele osób uważa, że to bardzo śmieszne. Jednak tak naprawdę JS podąża tu za standardem. Konkretnie, spełnia IEEE 754 - czyli standard liczb zmiennoprzecinkowych. Tak, liczb. Definiuje on arytmetykę zmiennoprzecinkową i zajmuje się również problemami takimi, jak reprezentowanie nieskończoności czy właśnie NaN. Został stworzony po to, by rozwiązać ciągle powtarzające się problemy w operacjach na liczbach zmiennoprzecinkowych. Chyba próżno szukać tu sensacji.

0.1 + 0.2 == 0.3
// false


No jak to? A spróbuj tego w języku, którego używasz. Jest spora szansa, że dostaniesz taki sam wynik. I to nie tylko w „niegodnych” językach, których używa się do weba, ale też w C++.

Znowu chodzi o operacje na liczbach zmiennoprzecinkowych, które muszą zostać zapisane jako zbiór zer i jedynek. To sprawia, że w momencie utrwalenia ich w pamięci może powstać błąd, który przy kolejnych działaniach na tych liczbach może się powiększać. Mam nadzieję, że wiesz, że nie możesz ufać floatom i nie używasz ich np. do zapisywania kwot ;)

9999999999999999
// 10000000000000000


A właśnie… JS nie ma czegoś takiego jak integer. Wszystko, co liczbowe, jest 64-bitową liczbą zmiennoprzecinkową podwójnej precyzji. Czasem może tej precyzji brakować. Zgodnie z IEEE 754 53 bity są znaczące, 11 to wykładnik, a jeden jest zarezerwowany na znak. To wystarczy na zapisanie liczby 9007199254740992 bez błędu. Tak samo zachowują się inne języki programowania (np. Java, o ile użyjemy double do zapisania tej liczby). Co natomiast jest u „konkurencji” lepsze to to, że można użyć innego typu do radzenia sobie z takimi przypadkami. 

Math.max() > Math.min() 
// false


Co powinien zwrócić Math.max()? Dobre pytanie, ale odpowiedź nie będzie taka jak się spodziewasz. Math.max() służy do porównywania przekazanych do tej funkcji wartości, a nie do zwracania największej liczby, jaką JS obsługuje. Z jakiegoś powodu Math.max bez argumentów zwraca -Infinity. Tak to jest zdefiniowane, jednak ze względu na przeznaczenie funkcji nie ma to większego znaczenia. Z ciekawostek mogę dodać, że na potrzeby max() w standardzie jest określone, że +0 jest większe niż -0.

'foo' % 2 == 0
// false
[] % 2 == 0
// true


O tym już wspominałem w artykule „Czemu nikomu niepotrzebny pakiet z npm ma 3 mln ściągnięć tygodniowo?” (i wyszukanie tej ciekawostki zainspirowało mnie do napisania tego wpisu). Operatory *, / i % starają się sprowadzić obydwie strony wyrażenia do liczby. Robią to przy użyciu wewnętrznej metody ToNumber.

W pierwszym przypadku sprawa jest dość prosta, bo ToNumber dla 'foo' zwróci NaN. Modulo z NaN to nadal NaN.

W drugim komplikacją jest to, że [] to obiekt. ToNumber będzie chciał koniecznie zamienić tablicę na prymitywną wartość. Zrobi to przy użyciu metody ToPrimitive - która finalnie wywoła toString(), w wyniku czego dostajemy pusty string - i ponownie wywoła na nim metodę ToNumber, co wypluje 0. Stąd 0 % 2, co wynosi 0. Ufff… wyjaśnione. W zasadzie chodzi o to, że JavaScript robi co może, żeby nie rzucić wyjątku, który by zatrzymał wykonywanie skryptu.



No to sobie pododajemy

Kolejną bardzo lubianą przez złośliwych kategorią testów jest dodawanie i odejmowanie do siebie różnych rzeczy w JavaScripcie. Główny problem polega na tym, że operator + - jak mówi specyfikacja - służy do dodawania liczb lub konkatenacji ciągów znaków. Mimo takiego przeznaczenia potrafi „dodawać” znacznie więcej:

[] + []
// ""


Tu sytuacja jest bardzo podobna, jak przy zagwozdce z operatorem modulo. Obydwie strony wyrażenia są obiektami, więc na początek JS przetworzy je na typy prymitywne. Wiemy już, że pusta tablica zrzucona na typ prymitywny to pusty string. Teraz nie pozostaje nic innego, jak połączyć dwa puste ciągi znaków w jedno.

[] + {}
// "[object Object]"
{} + []
// 0
({} + [])
// "[object Object]"
{} + {}
// "[object Object][object Object]" lub NaN


To jeden z bardziej pokręconych przykładów. Czemu []+{} zachowuje się tak, jakbyśmy przewidywali, a {}+[] daje zero?

W pierwszym przypadku plan gry obejmuje przerzucenie obydwu stron do typu prymitywnego i połączenie wartości - o ile którakolwiek z nich to string - albo dodanie ich, gdy obydwie są liczbami. W drugim coś widocznie poszło nie tak. Okazuje się, że w tym wypadku {} oznacza blok (ok, tego nie byłem w stanie znaleźć w standardzie, było googlowane). Ponieważ jest pusty, to finalnie nie jest wykonywane dodawanie, a +[]. Gdy otoczymy wyrażenie nawiasami ({}+[]) to będziemy mieli faktycznie operację dodawania.

Ostatni przypadek jest fascynujący. Na Chrome oraz Node.js (V8) wykonuje to dodawanie, ale tym samym jest niespójne z zachowaniem {}+[]. Na Firefox wynikiem jest NaN - czyli zachowanie jest spójne, {}+{} zamienia się na +{}.



Równości, nierówności

Kojarzycie może ten obrazek, gdzie budowa Trójcy Świętej jest porównana do pewnych właściwości JavaScriptu?



Zacznijmy od operatora ==. Pewnie wiecie, że nie jest to „silnie” sprawdzenie równości. Jest to operator, który jest w stanie porównywać różne typy i - by to robić - będzie wewnętrznie wykonywać różne konwersje. Unikniemy konwersji tylko, jeśli obydwie wartości będą tego samego typu - wtedy pod spodem zostanie wywołany operator ===. W każdym innym przypadku to, co się stanie, zależy od typów, jakie porównujemy. W zasadzie jeżeli będziemy mieli dwa różne typy po obydwu stronach, z których jeden jest liczbą lub boolem, to JavaScript dąży do porównania liczbowego i w taką stronę będzie dążyć z konwersją.

Operator != działa tak samo, tylko zwraca zanegowaną wartość. Prawdopodobnie tego się spodziewaliście ;) Przejdźmy więc do omówienia tych równości:

[] == 0


Już parę razy wspominałem o tym, że [] po zrzutowaniu do wartości prymitywnej to pusty string, a ten po zrzuceniu na liczbę to 0.

"0" == 0


Ponieważ mamy tu miks liczby i ciągu znaków to "0" będzie przekonwertowane na liczbę.

"\t" == 0


Tu znowu dzieje się to samo, a że literał, który zostanie przekonwertowany na liczbę, może zawierać białe znaki, to lewa strona jest przekonwertowana na 0. Dlatego wynikiem jest true.

[] != "0"
[] != "\t"


Konwersja obiektu po lewej na string. Finalnie to porównanie dwóch ciągów znaków, z których jeden jest pusty.

"0" != "\t"


Brak konwersji, zwykłe porównanie dwóch ciągów znaków.



Ten JavaScript - taki szalony

Bardzo łatwo się przyczepić do tego, że JS zachowuje się nieprzewidywalnie. Wydaje się, że za mocno stara się konwertować wartości - często w dziwny sposób z punktu widzenia programisty.

Czy natomiast jest sens skupiać się na tych dziwactwach, gdy ocenia się język? Nie sądzę. Wątpię, żeby akurat te właściwości powstrzymały kogoś przed zbudowaniem dobrej aplikacji. Jest o wiele więcej innych własności ważniejszych z punktu widzenia programisty i to, że "0" == 0 zwróci true tego nie zmieni.

Na koniec jeszcze tylko przytoczę tweet Chrisa Heilmanna, który jest doskonałym podsumowaniem tego artykułu: