Kot Źródłowy

Wszystkie dziwactwa JS-a i inne rzeczy, za które go kochamy

Cześć. Dzisiaj przychodzę z czymś trochę bardziej zakręconym. Jakiś czas temu umieściłam na Kocim Fanpejdżu post z różnymi dziwactwami języka JavaScript. Tak się jednak składa, że te wszystkie linijki kodu można całkiem logicznie wyjaśnić, a kiedy wiemy, skąd się biorą, jest nam dużo łatwiej ogarnąć resztę języka i stajemy się lepszymi programistami. Jeśli ktoś jeszcze nie widział posta, to niniejszym go tutaj wklejam:

Postanowiłam więc, że ci wyjaśnię, dlaczego niektóre operacje działają w JS tak, a nie inaczej i właściwie, co z tego wszystkiego wynika. Gotowy na tę pasjonującą podróż za czerwoną kropką w odmęty JS-a? To zaczynamy!

typeof ‘NaN’

Otóż wiele osób dziwi, że wyjściem z takiej operacji jest number. Wiele na ten temat można znaleźć w specyfikacji, która wyraźnie mówi, czym są dla JS-a liczby:

Number type set of all possible Number values including the special “Not-a-Number” (NaN) value, positive infinity, and negative infinity

Czym jednak jest ta magiczna wartość Not-a-Number wobec wielkiego bezkresu wszechświata? Tego nie dowiemy się nigdy, ale ogólnie, o co z nią chodzi, to już łatwiej. Gdybym miała wytłumaczyć to najprościej, jak się da, NaN to jest taka wartość, która próbuje być liczbą, ale jej nie wychodzi. Chcesz wiedzieć więcej? Oto przykłady:

  • Wychodzimy poza zakres liczb obsługiwanych przez JS.
  • Próbujemy zamienić łańcuch tekstowy na liczbę, np. parseInt('miau')
  • Chcemy podzielić zero przez zero
  • Chcemy pomnożyć zero razy nieskończoność

Jeszcze kilka innych przypadków, w których wyniku działania nie da się określić za pomocą liczb rzeczywistych z zakresu obsługiwanego przez JS.

9999999999999999 === 10000000000000000

Tu znowu zdziwienie, skąd właściwie się bierze taki absurd. Odpowiedź jest podobna, jak w poprzednim przypadku. W języku JavaScript zostały zastosowane liczby zmiennoprzecinkowe, zapisywane za pomocą standardu IEEE 754, który, tak jak każdy standard zapisu liczb, ma swoje ograniczenia. Powyżej pewnej granicy liczby naturalne (integer) przestają być dokładne. Jak sprawdzić tę granicę? Biblioteka standardowa JS-a udostępnia nam dwie takie stałe, w obrębie których możemy bezpiecznie wykonywać obliczenia.

Number.MAX_SAFE_INTEGER < 9999999999999999
Number.MIN_SAFE_INTEGER > -9999999999999999

Na żywym przykładzie widać, że wychodzenie poza nie nie jest zbyt dobrym pomysłem, jeśli zależy nam na bardzo dokładnych obliczeniach.

0.2 + 0.1 !== 0.3 ale 0.5 + 0.1 === 0.6

Kolejna rzecz, która wydaje się superdziwna, ale to również nie jest wina samego JS-a, a jedynie tego, w jaki sposób są zapisywane liczby w systemie binarnym. Jak sobie z tym poradzić? Wszystkie wyniki można podawać do jakiegoś sensownego miejsca po przecinku.

Math.max() === -Infinity i Math.min() === Infinity

To już zaczyna wyglądać, jak zło w czystej postaci. Jednak tak naprawdę trzeba sobie zadać pytanie, do czego służą obie te funkcje. Wiesz? Jeśli nie, to już ci mówię. Ich zadaniem jest kolejno wybranie największej i najmniejszej liczby z danego zbioru liczb przekazanych jako argumenty. O tym, jak działa taki algorytm, pisałam w poprzednim poście.

[] + [] === “”

Tutaj wchodzimy w nową rodzinę udziwnień i nowy ich powód. Jeśli przypomnisz sobie swój pierwszy kurs JS-a, tutorial lub przeczytaną na ten temat książkę, to z pewnością tam było napisane, że JS to język dynamicznie typowany. Co to znaczy dla nas w praktyce? Że my nie martwimy się o typ zmiennej, tylko przypisujemy jej wartość, a silnik JS-a sam określi, z jakim typem mamy do czynienia.

let a = 124;
let b = 'meow';
typeof a // number
typeof b // string

W jaki sposób JS to robi? Udaje mu się to za pomocą mechanizmu “duck typing”. O tym, jak to działa, może nam powiedzieć już pewien cytat:

Jeżeli to chodzi jak kaczka i kwacze jak kaczka, to musi być kaczką.

Jak to się ma do naszego przykładu? Tak się składa, że tablic w JS nie możemy dodać do siebie przy pomocy operatora +. Możemy jednak w ten sposób połączyć dwa łańcuchy tekstowe. Mądry silnik JS-a zatem dynamicznie rzutuje tablice na puste stringi, potem łączy je ze sobą i na wyjściu dostajemy pusty string. Czyż to nie jest logiczne? Zapewniam cię, że później będzie jeszcze ciekawiej.

[] + {} === ‘[Object object]’

Tutaj już mogłoby się wydawać, że dzieją się rzeczy niestworzone. Zasada jest jednak podobna, jak w poprzednim przykładzie. Jak wiemy, to tablica nie może być do czegoś dodawana. Zostaje więc skonwertowana na pusty string. Tym samym obiekt też jest konwertowany na string. A tak właśnie wygląda tekstowa reprezentacja obiektu. Nie wierzysz, to sprawdź sam.

String({})

{} + [] === 0

No dobra, jeśli to poprzednie zniosłeś jakoś bez utraty poczytalności, to obecny przykład może wywołać fobię do programowania! No bo jak to, dodawanie nie jest przemienne? Ano jest, tylko pamiętaj, że poprzednio nie dodawaliśmy, a jedynie łączyliśmy (konkatenowaliśmy) dwa łańcuchy znaków. Czy teraz dodajemy? Oczywiście, że nie! To byłoby zbyt proste! Ten plusik zmienia nam tutaj pustą tablicę na liczbę. Na pewno znasz te magiczne sposoby:

+ '27' // 27
!! 1   // true
'' + 777 // '777'

Jeśli nie, to właśnie je poznałeś. Przydają się czasami, żeby nie musieć pisać funkcji rzutujących.

Pewnie pomyślisz, że dalej coś ci nie pasuje. No bo mamy jeszcze ten pusty obiekt. Ha! Tu cię JS znowu oszukał. Bo tak się składa, że {} to nie tylko pusty obiekt. Czasem zdarza się w ten sposób deklarować blok kodu. I w tym momencie parser traktuje to jako pusty blok kodu, który nie robi absolutnie nic. Co za tym idzie, jest on ignorowany w tej operacji.

true + true + true === 3 i true-true === 0

Nie tylko w JS true jest utożsamiane z liczbą 1. Dlatego nikogo nie powinno dziwić, że 3 * true daje nam 3. Tak samo `1 - 1 === 0 co nikogo nie powinno dziwić.

true == 1 ale true !== 1

Tutaj kłania się rozumienie, na czym polega różnica pomiędzy operatorem równości == i identyczności ===. Ten drugi nie zezwala na niejawną koercję typów i dlatego właśnie true, które jest typem logicznym, nie może stać się 1 (liczbą).

(!+[]+[]+![]).length === 9

No to już jest prawdziwe combo. Ale pocieszę cię, jeśli to przetrwasz, to już zbyt wiele rzeczy cię nie zaskoczy w programowaniu. A przynajmniej w tym poście. Żeby to zrozumieć, to znowu trzeba rozbić wyrażenie w nawiasie na kawałki. Łatwiej będzie mi wytłumaczyć, jakie mechanizmy tu po kolei zachodzą, jeśli będziemy rozpatrywać to wyrażenie niejako od końca. Wobec tego zaczynamy:

  • ![] - daje nam false. Dlaczego? Tablica jest tak naprawdę obiektem, a nie typem prostym. Każdy obiekt zrzutowany do typu logicznego daje true. Wobec tego zaprzeczenie true daje nam false.

  • []+![] to tak naprawdę []+false. Chyba już spotkaliśmy się z podobnym wyrażeniem. Zamieniało ono pustą tablicę na pusty string. Wynikiem tego wyrażenia jest więc 'false' jako string.

  • !+[] ten kawałek jest równie nieoczywisty, jak poprzedni. Jednak po dłuższym zastanowieniu możesz zauważyć, że + tutaj to nie jest dodawanie, a rzutowanie tablicy na liczbę, czyli wynik będzie równy 0, a z kolei, gdy zaprzeczymy zero, to dostaniemy true.

  • Tym sposobem dotarliśmy do możliwie uproszczonej wersji nawiasu. Wygląda ona teraz ni mniej. ni więcej tak: (true + 'false') – czego wynikiem jest konkatenacja stringów, czyli 'truefalse. Długość wyjściowego stringu wynosi 9.

Reszta przykładów wydaje mi się na tyle łatwa i oczywista, że nie warto sobie nimi zaprzątać głowy. Myślę, że jeśli wystarczająco dobrze przeanalizowaliście poprzednie przykłady, to z tymi kilkoma na pewno sobie poradzicie. Jeśli chcecie, możecie się pochwalić w komentarzach, co wam wyszło.

PS: Tak w ogóle to jest akcja. Koci Fanpejdż niedługo będzie miał 1024 osoby (to naprawdę dużo). Dlaczego o tym wspominam? Bo obiecałam Fejsbukowym kotom, że będą miały swoją grupę, gdy tylko ich liczebność przekroczy jeden kilobajt. Można w komentarzu wpisać proponowaną nazwę grupy, o tutaj:

Zapraszam też do dołączania do naszej wesołej gromadki. Z niej najprędzej się dowiecie, co się dzieje w życiu bloga. Tymczasem trzymajcie się ciepło i nie gubcie parasola w komunikacji miejskiej.

Miau!