Top 10 kuriozalnych zjawisk w JavaScript

Kuriozum, to wg wikipedii rzecz lub zjawisko wyjątkowo osobliwe, które budzi zdumienie swoją niezwykłością lub dziwacznością. I zjawisk takich w JavaScripcie nie brakuje.

Język, który został zaprojektowany przez Brendana Eicha w 10 dni (21 lat temu !) jest jednocześnie obecnie najpopularniejszych językiem githuba (pod względem liczby założonych repozytoriów). Lata lecą, kolejne specyfikacje niby wychodzą, a wszystko co poniżej można uruchomić w szanowanym przez większość developerów Google Chrome w wersji 53..

Zapraszam na krótką podróż po konstrukcjach, które wielu programistów JS mogą nieźle zaskoczyć. Nie róbcie tego na produkcji.

Konstruktor Date

W sumie niby według kalendarza gregoriańskiego miesięcy jest 12, mają swoje nazwy i swoją kolejność, ale datę w JS można też stworzyć na przykład z miesiącem numer 99.

new Date(2000, 99, 10) //Thu Apr 10 2008 00:00:00 GMT+0200 (Środkowoeuropejski czas letni)

Krótka zabawa z tym konstruktorem prowadzi do następujących wyników:

new Date(2000) //Thu Jan 01 1970 01:00:02 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, 0, 0) //Fri Dec 31 1999 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, 1, 1) //Tue Feb 01 2000 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, 0, 1) //Sat Jan 01 2000 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, -10, 0) //Sun Feb 28 1999 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, 1, null) //Mon Jan 31 2000 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, null, 1) //Sat Jan 01 2000 00:00:00 GMT+0100 (Środkowoeuropejski czas stand.)
new Date(2000, undefined, 1) //Invalid Date

A więc istnieje taki obiekt, jak “Invalid Date”, ale w kontekście pozostałych, podobnie irracjonalnych wywołań brakuje tu chyba jakiejś konsekwencji.

Porównania

Zasada wydaje się prosta. Mamy do dyspozycji strict comparison (===), czyli porównanie typu i wartości oraz abstract comparison (==), który konwertuje dwa operandy do tego samego typu, żeby wykonać porównanie. Ponieważ JS jest językiem dynamicznie typowanym, to porównywać można dosłownie wszystko. Jeżeli zapomnimy trzeciego znaku równości, to efekty mogą być zaskakujące.

[] == 0 //true
[] == [] //false
[[]] == false //true
[[]] == [] //false
[1] == "1" //true
null == false //false
14 == {valueOf:_=>14} //true
"hello world" == {toString:_=>"hello world"} //true
NaN == NaN //false
NaN === NaN //false

NaN jak widać zajmuje szczególne miejsce w świecie JavaScript. Jest on oczywiście wynikiem pewnych nieudanych operacji arytmetycznych, które nie chciały rzucić wyjątkiem. Jak to niektórzy żartują, “NaN stands for Not a NaN” (link).

isNaN

No to jak sprawdzić, czy coś jest NaN ? Funkcją isNaN. Obecnie mamy do dyspozycji dwie takie funkcje. Dlaczego ? Bo stare window.isNaN potrafi zaskoczyć:

isNaN("I really don't feel like a NaN") //true
isNaN(["Array shouldn't be a NaN"]) //true
isNaN({a:"Can Object be a NaN ?"}) //true

I dlatego właśnie, żeby uniknąć powyższych błędów, ES6 wprowadził Number.isNaN.

Arytmetyka

Jak już zostało wspomniane, NaN można uzyskać w wyniku nieudanych operacji matematycznych. O ile takie rzeczy mogą się wydawać naturalne:

Math.sqrt(-2) //NaN
Math.log(-3) //NaN
0/0 //NaN

..o tyle poniższe zjawiska wytłumaczyć już znacznie trudniej:

0.1+0.2 //0.30000000000000004
1/0 //Infinity
1/Infinity //0
1/-Infinity //-0
1/1e-300 //9.999999999999999e+299
1/1e-308 //1e+308
1/1e-309 //Infinity
Number.MAX_VALUE === Number.MAX_VALUE + 1 //true
(Number.MAX_SAFE_INTEGER + 1) === (Number.MAX_SAFE_INTEGER + 2) //true
(Number.MAX_SAFE_INTEGER + 2) === (Number.MAX_SAFE_INTEGER + 3) //false

Analiza matematyczna niby mówi o tym, że granica ciągu 1/n przy n zmierzającym do zera wynosi nieskończoność, ale każdy wie, że dzielenia przez zero unikać należy.

Słynne 0.1+0.2 wynika oczywiście z reprezentacji liczb zmiennoprzecinkowych i specyfiki operacji na nich. Uważam, że powinno być pierwszym pytaniem na każdym teście Turinga (link).

Konwersje typów

Konwersje występują nie tylko przy wspomnianych wcześniej porównaniach. Unarny operator bang służy do konwersji do booleana, unarny operator plusa konwertuje do numbera. Sam plus zresztą służy też do dodawania lub konkatenacji w zależności od tego, jakiego typu są operandy (albo do czego może je skonwertować). Kilka ciekawych przykładów konwersji:

'5'-2 //3
'5'+2 //"52"
+[] //0
2+[] //"2"
2+![] //2
+[![]] //NaN

Ostatnie wyrażenie zwraca NaN, gdyż nieudaje się konwersja do liczby z [false]. Za pomocą bang, plusa i nawiasów można w JS obfuskować / enkodować kod. Konwerter można znaleźć na stronie www.jsfuck.com. Kilka przykładów poniżej:

[]+[] //""
+[] //0
+!+[] //1
[+!+[]]+[+[]] //"10"
+([+!+[]]+[+[]]) //10

Wszystko można sobie nadpisać

Przeglądarkowy window udostępniający wszystkie globalne funkcje nie jest w żaden sposób enkapsulowany i nadpisanie czegokolwiek jest dziecinnie proste. Kilka pomysłów na zrobienie żartu kolegom z pracy:

Math.random = _=>-.1;
Number.isNaN = window.isNaN;
Math.sqrt = x => Math.sqrt(x); //zabija rekurencją
Array.prototype.slice = Array.prototype.splice;

 

return może się wywołać kilka razy

Konstrukcja z cyklu “Oszukać przeznaczenie”, w dwóch wariantach. Z punktu widzenia np. C# niedopuszczalna przez kompilator.

let f1 = () => {
   try{
      return true;
   }
   finally{
      return false;
   }
};

f1(); //false


let f2 = () => {
   for(let i = 0; i < 5; i++) {
      try{
         return i;
      }
      finally{
         if (i != 3) continue;
      }
   };
};

f2(); //3

 

Konstruktor może zwracać wartości

ECMAScript 2015 wprowadza klasy ze słowem kluczowym constructor. Nie jest to jednak do końca taki konstruktor jak np. w C++, bo może on swobodnie zwracać wartości, nawet takie, które są innymi obiektami:

class YouCannotInstantiateMe{
   constructor(){
      return {toString:_=>"I am another object"};
   }
}

new YouCannotInstantiateMe().toString() //"I am another object"

 

Hoisting i Temporal Dead Zone

Hoisting to mechanizm dobrze znany w “starym” JS. Prosty przykład:

(function() {
    console.log(x); //undefined
    var x = 1;
}());

Wartość undefined wypisuje się z powodu hoistingu właśnie. Samowywołująca się funkcja definiuje scope, w nim definiujemy x i jest on hoistowany na początek tego scope’u (funkcji). Hoistowana jest tylko definicja, bez przypisania wartości, stąd wartość undefined. Oczywiście, gdybyśmy odwołali się do innej zmiennej, niezdefiniowanej nigdzie w tym scope, otrzymujemy ReferenceError.

Co nowego w ES2015 ? Dochodzą słowa kluczowe const i let, które mają niejako wyprzeć stary sposób definiowania poprzez var. Po przepisaniu powyższego kodu na let:

(function() {
    console.log(x); //VM868:2 Uncaught ReferenceError: x is not defined(…)
    let x = 1;
}());

…dostajemy ReferenceError. Koniec hoistingu ? Wręcz przeciwnie, hoisting według specyfikacji istnieje nadal, to znaczy zmienne są tworzone u góry scope’a, ale nie mamy do nich dostępu do czasu, aż wykonany zostanie na nich LexicalBinding, czyli po prostu przypiszemy wartość. Przestrzeń w kodzie pomiędzy wejściem do scope’a (np. funkcji), a deklaracją zmiennej określana jest jako Temporal Dead Zone. Występuje ona również dla klas.

Niepodrabialny document.all

Kiedy już nauczymy się tych wszystkich reguł i wyjątków to na scenę wchodzi document.all, cały na biało oczywiście. Na początek kilka słów ze specyfikacji:

The all attribute must return an HTMLAllCollection rooted at the Document node, whose filter matches all elements.

I rzeczywiście kolekcja elementów DOM jest zwracana:

document.all //HTMLAllCollection[7696]

Problem leży w tym:

!!document.all //false 

…w tym:

typeof document.all //"undefined"

..i w tym, że w sumie to jest on też funkcją, działającą jak document.getElementById

document.all("root") //<dfn id="root">..</dfn>

 

Jeśli macie podobne przykłady, wrzucajcie proszę w komentarzach.

Jeśli ktoś chce pozostać na bieżąco z postami na blogu zapraszam do followowania na twitterze lub przez feedly.

7 thoughts on “Top 10 kuriozalnych zjawisk w JavaScript

    • Zgadza się i bardzo fajnie, że o tym napisałeś.

      Problem może wystąpić, gdy brakuje nam wiedzy z JS, mamy background w innych językach i założymy sobie, że 0 to co innego niż 0.0. W JS oczywiście mamy do dyspozycji tylko typ number, który tak, jak napisałeś jest floating point.

      Pozdrawiam

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s