WTFJS - rozjaśnienie umysłu

Blog WTFJS o dziwactwach w JavaScriptcie widział już pewnie każdy. Niektóre przykłady są naprawdę fajne :) Co więcej — niektóre działają mi w sposób przedstawiony przez autorów tylko czasami, choć to pewnie nie problem JavaScriptu, a Firebuga :).

Jako, że lubię wiedzieć dlaczego tak to działa, a nie inaczej, staram się wyjaśnić sobie o co chodzi w każdym przykładzie. Niektórych nadal nie rozumiem, część jest oczywista, część ma rozwiązania podane na stronie, a te ciekawsze postaram się wyjaśnić w tym (i może następnych) wpisach.

Jestem prawie obiektowy

typeof "abc" == "string" // true
typeof String("abc") == "string" // true
String("abc") == "abc" // true -- same types get casted to equal each other
 
String("abc") instanceof String // false -- hmmm...
(new String("abc")) instanceof String // true
String("abc") == (new String("abc")) // true -- wait, wtf?

new String() to nie String(), a "string" to nie "object". Proste? :) Trochę wolniej:

Pierwsza linia jest oczywista, ale warta zapamiętania — typem literału stringa jest "string". W drugiej mogłoby się wydawać że tworzymy obiekt, ale nie — konstruktor String wywołany bez new po prostu rzutuje na stringa. Trzecia linia tylko to potwierdza.

Czwarta linia to nawiązanie do tytułu jaki nadałem tej sekcji — Jestem prawie obiektowy. Autor tego przykładu zdziwił się skąd takie zachowanie, ponieważ pewnie nie wiedział o typach prostych w JavaScriptcie. Otóż wartości: 1, "a", true nie są typu "object" tylko odpowiednio "number", "string", "boolean". Nie są z tego powodu instancjami swoich podobnie brzmiących z nazwy klas (ale uwaga — czerpią z ich prototypów). Jest to zachowanie całkiem uzasadnione (choć niefajne w działaniu), ponieważ w innym przypadku (gdyby typy proste nie istniały) nie działało by porównanie ===.

Wyobraźmy sobie sytuację, w której pisząc "a" tworzymy nowy obiekt typu "string". Porównanie dwóch zmiennych, które są obiektami za pomocą operatora === sprawdza, czy zmienne te odnoszą się do dokładnie tego samego obiektu w pamięci. Tak więc w naszej hipotetycznej sytuacji otrzymalibyśmy:

"a" === "a"; // -> false

Uups. Po lewej obiekt, po prawej obiekt, ale to nie te same obiekty. Na szczęście teraz jest tak:

"a" === "a"; // -> true
new String("a") === new String("a"); // -> false
true === true; // -> true
new Boolean(true) === new Boolean(true); // -> false

Nie twierdzę, że jestem fanem takiego rozwiązania. Nie jestem pewien, ale chyba w Javie jest podobnie, za to na przykład już w Rubym jest to rozwiązane lepiej (ktoś potwierdzi?). Cóż — nic nie jest idealne ;).

Wróćmy jednak do przykładu. W piątej linii wreszcie tworzymy obiekt typu String. Dla przypomnienia:

typeof new String("a"); // -> "object"
typeof String("a"); // -> "string"

Ostatnia linia powinna być już oczywista jeśli powiem, że operator == przeprowadza konwersję zmiennych stojących po obu jego stronach do jednego typu (którego, tego nie wiem — pewnie zmiennej po lewej — kto ma ochotę niech sprawdzi w specyfikacji i napisze w komentarzu :). Tak więc z porównania String("abc") == (new String("abc")) otrzymujemy porównanie "a" == "a". Dla porównania:

String("abc") === (new String("abc")); // -> false

Widać więc, że trzeba uważać na krótki operator porównania i stosować go z głową. Polecam przyzwyczajenie się do używania potrójnego operatora i tylko w szczególnych wypadkach, kiedy na przykład znamy typy zmiennych (typeof x == "string"), używanie podwójnego.

Jestem prawie obiektowy, ale umiem porównywać

Kolejny przykład będzie tylko rozwinięciem poprzedniej części. Oto on:

(1) === 1; // true
Number.prototype.isOne = function () { return this === 1; }
(1).isOne(); // false!
Number.prototype.reallyIsOne = function () { return this - 1 === 0; }
(1).reallyIsOne(); // true

Pierwsza linia powinna być oczywista — nawias nie ma znaczenia. Druga i trzecia to ścisłe nawiązanie do poprzedniej części. W isOne typeof this; // -> "object". Mimo, że wywołujemy metodę na zmiennej typu "number", to jest ona po cichu rzutowana na "object" — dziwne, ale do zrozumienia — ciężko żeby this nie było obiektem.

Dwie ostatnie linie, to dynamiczne rzutowanie. Operacja odejmowania stara się rzutować obie zmienne na typ liczbowy. this jest rzutowane na 1 i reszta jest jasna. Równie dobrze można byłoby zapisać reallyIsOne w ten sposób:

Number.prototype.reallyIsOne = function () { return +this === 1; };

Używaj średników!

Poprzedni przykład przekleiłem na czysto z WTFJS. Jeśli będziecie go w tej postaci wklejać do Firebuga, to możecie się spotkać z latającymi wyjątkami. Skąd ten problem? Na końcu drugiej i czwartej linii brakuje średników i przynajmniej firefoksowy silnik wysiada. Rada na przyszłość — średniki dobre są.

Niezrozumiałe dla mnie

Tego już nie wytłumaczę :). Choć — obstawiałbym, że problem leży gdzieś w standardzie reprezentacji liczb zmiennoprzecinkowych, ale to tylko mój domysł.

Number.MAX_VALUE*1.0000000000000001 === (1/0) // false
Number.MAX_VALUE*1.0000000000000002 === (1/0) // true

Komentarze do notki “WTFJS - rozjaśnienie umysłu”

  1. Szymon 

    na opisywaną tu minę (mimo że nie umiem js) nie wlazłem bo robiąc sobie własny dispacz używałem porównywania konstruktorów a nie typeof
    ["a".constructor === String, new String("a").constructor === String] => true,true
    jak mawiają głupi ma szczęście. Btw, osobiście nie widzę problemu z literałami i ===, rozwiązanie jest proste, chętnie bym się dowiedział od Brendana czemu się na nie nie zdecydował. Btw, zawsze mnie też ciekawiło dlaczego js nie może mieć tzw. numerical tower. Zresztą takich pytań uzbierało by się dużo, pewnie się bardzo spieszyli z wydaniem kolejnej wersji netszkapy ;f Mogli w takim wypadku dać sioda a nie tworzyć jakieś kalectwa.
    Worse is Better.

  2. .kropelka 

    wydawało mi się, że to ostatnie to tylko kwestia zaokrągleń...
    ale wyszedł mi jeszcze jeden kwiatek :/
    Number.MAX_VALUE+Number.MAX_VALUE*0.0000000000000001 == Number.MAX_VALUE*1.0000000000000001 //false
    Number.MAX_VALUE+Number.MAX_VALUE*0.0000000000000001 === Number.MAX_VALUE*1.0000000000000001 //false

  3. .kropelka 

    sorry, za nieczytelność ;)
    miało być
    var x = 0.0000000000000001;
    Number.MAX_VALUE+Number.MAX_VALUE*x == Number.MAX_VALUE*(1+x)

  4. Rafał 

    Ostatni przykład jest IMO związany ze sposobem reprezentacji liczb w JS (IEEE754).
    Nie da się jednoznacznie przedstawić liczby 1e-16 jako liczby podwójnej precyzji (aby jednoznacznie ją przedstawić wymagane byłyby conajmniej 54 bity mantysy), przy czym dokładność dla większości implementacji wynosi 53 bity. Dla liczby 2e-16 wystarczą 53 bity. W ten sposób liczba 1.0000000000000001 zostanie sprowadzona do 1. Jeśli się mylę, niech mnie ktoś poprawi.

  5. Piotrek Reinmar Koszuliński 

    @Szymon: gdzieś czytałem historię JSa i było wyjaśnione skąd trochę dziwnych decyzji. Nie pamiętam tylko niestety gdzie.

    @.kropelka: Niezłe ;>

    @Rafał: bardzo możliwe. Trzeba by niestety poznać kiedyś ten standard, bo to nie tylko problem JS.

  6. Szymon 

    @Piotrek Reinmar Koszuliński, me czytał w xsiążce "Coders at Work", o js zaczyna się na stronie 157. Tylko winni się tłumaczą ;) to jest straszny gniot imho (nawet biorąc pod uwagę założenia). No ale tłumaczenie "nie było czasu" jak widać załatwia wszystko :F

  7. Vokiel 

    Ostatni przykład wydaje się być związany z tym co pisał Rafał.
    Przykładowo w fierbugu:
    <pre lang="javascript">
    var a = 1.0000000000000001;
    console.log(a);
    // 1
    var b = 1.0000000000000002;
    console.log(b);
    // 1.0000000000000002

    // Zatem
    Number.MAX_VALUE*1.0000000000000001;
    // jest de facto
    Number.MAX_VALUE*1
    // czyli MAX_VALUE
    // a już
    Number.MAX_VALUE*1.0000000000000002;
    //jest, tym co jest, czyli wychodzi poza MAX_VALUE i daje
    Infinity
    </pre>

    Oczywiście, (1/0) daje Infinity

Zostaw odpowiedź