„Temná strana“ JavaScriptu

I když JavaScript používáte řadu let, můžete v něm narazit na místa, která vás překvapí, a kterým nerozumíte. Na některá taková temná místa si posvítíme v tomhle článku, zejména na logické hodnoty a operátory, operátor rovnosti, středníky aj.

Typy

Pro pochopení dalšího textu je nezbytné mít jasno, jaké jsou v JavaScriptu typy hodnot. Zde je krátká rekapitulace:

  • undefined – má jedinou hodnotu undefined. Proměnná, které ještě nebyla přiřazena hodnota, má právě tuto hodnotu.
  • null – má jedinou hodnotu null, která reprezentuje chybějící objekt.
  • boolean – je typ pro logické hodnoty true a false.
  • string – textový řetězec
  • number – je 64bitové číslo (s plovoucí desetinnou čárkou). Tento typ rovněž zahrnuje hodnoty NaN, +Infinity−Infinity.
  • object – Objekt je kolekce vlastností (vše ostatní, s čím se v JavaScriptu setkáte).

Pokud budeme mluvit o primitivních typech, jsou to všechny typy, kromě object.

Pro informaci o typu hodnoty v proměnné lze využít operátor typeof, který vrací řetězec obsahující název typu. Jen pozor, pro hodnotu null vrací 'object' (protože null se vyskytuje tam, kde očekáváme objekt), a v případě funkcí (což jsou také objekty) vrací 'function'.

Logické operátory

Logické operátory &&, || a ! se typicky používají pro hodnoty typu boolean, přičemž výsledkem operace je opět logická hodnota. Jak se však chovají pokud operátory použijeme na jiné než logické hodnoty?

    'pat' && 'mat' // vrací 'mat'
    'karl' || 'egon' // vrací 'karl'
    null || 0 // vrací 0
    undefined && false // vrací undefined
    !!'hello' // vrací true

Operátory se řídí těmito pravidly:

  • Operátor && vrací první operand, pokud může být převeden na false, jinak vrací druhý operand.
  • Operátor || vrací první operand, pokud může být převeden na true, jinak vrací druhý operand.
  • Operátor ! vrací false, pokud operand může být převeden na true, jinak vrací true.

Operátor || se s oblibou používá jako zkratka foo = opt_foo || create_foo()  za

    if (opt_foo)
        foo = opt_foo
    else
        foo = create_foo()

Operátor ! se dá zneužít na převod libovolné hodnoty na hodnotu typu boolean, tedy !!0 === false. Čitelnější je však použít Boolean(0) === false.

Převod na logické hodnoty

V předchozím odstavci jsme často používali převod na hodnotu typu boolean. Vysvětleme si, jak tento převod funguje, tj. co je výsledkem výrazu Boolean(expr). Výsledek je false, pokud vyhodnocený výraz expr nabývá jedné z těchto hodnot: undefined, null, false, 0, -0, NaN, nebo prázdný řetězec  ''.

V ostatních případech (např. '0', 'false', [], ' ', libovolný objekt tedy i new Boolean(false)) je výsledek true.

S převodem na logickou hodnotu se setkáváte velmi často, nejen v již zmíněných případech, také u příkazů if, while nebo u podmiňovacího operátoru ( ? : ).

Operátor rovnosti

Operátor rovnosti == vrací vždy hodnotu typu boolean. Vrátí-li true, říkáme, že nastala rovnost.

Kdy nastává rovnost? Pokud jsou operandy stejného typu, je pravidlo jednoduché: hodnoty se musí shodovat (s výjimkou čísla NaN, které se nerovná ničemu, a  +0 se rovná -0), v případě objektů musí být referencemi na stejný objekt.

    NaN != NaN
    0 == -0
    {} != {}

Pokud neporovnáváme hodnoty stejného typu, je vypadá působení operátoru trochu magicky, posuďte sami:

  1.  
        null == undefined
  2.  
        0 == ' rn'
        '0' == false
        '  0x01' == true
        '2' != true
        '2' != false
        'true' != true
        'true' != false
        0 == []
        0 == [0]
        0 != {}
        2 == [2]
        false == []
        true != {}
        false != {}
  3.  
        '0' == [0]
        '2' == [2]
        '' == []
        '' != {}
  4.  
        0 != null
        null != true
        null != false
        'null' != null
        'undefined' != undefined
        undefined != true
        undefined != false

JavaScript se řídí těmito pravidly pro určení výsledku:

  1. null se rovná undefined.
  2. Pokud je jeden operand typu number nebo boolean a druhý number, boolean, string nebo object, jsou operandy převedeny na čísla.
  3. Pokud je jeden operand typu string a druhý object, je ten druhý převeden na string. K tomu se použije metoda valueOf, příp. toString, pokud ta první nevrací primitivní typ nebo není vůbec definovaná. U nativního Array je výsledkem metody toString to stejné jako  join(',').
  4. V ostatních případech nastává nerovnost.

Operátor rovnosti není vždy tranzitivní, například [1] == 1 a 1 == new Boolean('0'), ale [1] != new Boolean('0'). Nicméně vždy platí, že pokud a == b, pak b == a.

K bodu 2 zbývá vysvětlit, jak dochází k převodu na čísla. Začneme příklady:

    Number(true) // 1
    Number(false) // 0
    Number(' rn') // 0
    Number('true') // NaN
    Number('125 1') // NaN
    Number('  5e1  ') // 50
    Number([]) // 0
    Number([2]) // 2
    Number({}) // NaN
    Number({valueOf: function() {return 1}}) // 1
    Number(undefined) // NaN
    Number(null) // 0
  • boolean – Hodnoty jsou převedeny na 01.
  • string – Pokud řetězec obsahuje literál pro číslo (libovolný počet bílých znaků před a po se ignoruje), je výsledkem toto číslo, jinak NaN. Pokud je ovšem literál prázdný (tj. původní řetězec je prázdný, nebo obsahuje jednom bílé znaky), je výsledkem  0.
  • object – Při převodu objektu na číslo se používá jeho metoda valueOf, příp.  toString.

Teď už je bez pochyb jasné, proč je třeba být při použití tohoto operátoru velmi opatrný, jinak si zaděláváte na těžko odhalitelné chyby. Rovněž si všimněte velkého rozdílu mezi  if (!a)if (a == false)  ne­bo mezi  if (a)if (a == true).  Pokud chcete testovat jen logickou hodnotu, používejte v obou případech první možnost a ne operátor  ==. Jinak se obecně doporučuje využívat operátor striktnější rovnosti  ===, který vrací  false, pokud se typy operandů liší. Pro zajímavost i CoffeeScript kompiluje svůj operátor  ==  do JavaScriptového operátoru  ===. Dodejme, že u jednoduché rovnosti lze typové porovnání vynutit například takto:

  • Převod na string před porovnáváním: '' + a == '' + b
  • Převod na number před porovnáváním: +a == +b
  • Převod na boolean před porovnáváním: !a == !b

Středníky

Specialitou JavaScriptu je jeho automatické doplňování středníků, díky kterému nemusíme psát středník za každý příkaz. Pokud se rozhodnete ponechat doplňování středníků na interpretru, musíte dávat pozor na místa, která bez středníku dávají jako jeden příkaz smysl, například (na podobný problém často narazíte při spojování více javascriptových souborů do jednoho):

    MyClass.prototype.myMethod = function() {
        return 42
    }

    (function() {
        // Inicializace ve scope anonymní funkce
    })()

Zjednodušeně řešeno JavaScript vkládá středníky tam, kde nerazí na token, který nepovoluje jeho gramatika, ale ne všude – jen na konce řádků nebo před uzavírací složenou závorku. Uvedené pravidlo se dá přeformulovat jako obměněné tvrzení, tedy JavaScript nevkládá středník na konec řádky, když token na následující řádce vyhovuje gramatice jazyka (a nebo nevyhovuje, ale vložením středníku se to nespraví). Konkrétně středník nebude vložen, pokud:

  1. Příkaz má neuzavřenou závorku, literál pro pole nebo objekt nebo končí jiným nevalidním způsobem (například tečkou nebo čárkou).
  2. Řádka končí for(), while(), do, if(), nebo  else.
  3. Řádka obsahuje jen -- nebo ++ (v tom případě je interpretován jako prefix operátor).
  4. Další řádka začíná s [, (, +, *, /, -, ,, . nebo jiným binárním operátorem.

Příklady k jednotlivým bodům:

  1.  
        var a, // zde nebude vložen středník
        b, c
  2.  
        if (a)
        do_something() // tato funkce bude zavolána jen při splnění podmínky
  3.  
        i // zde bude vložen středník kvůli "restricted production" - viz dále
        ++ // zde nebude vložen středník
        j // hodnota j bude inkrementována
  4.  
        foo()
        [1,2,3].forEach(bar) // TypeError pokud pod indexem 3 návratové hodnoty foo nebude pole

Kromě toho existují v gramatice JavaScriptu takzvané „restricted production“, které zakazují na jistých místech použití konce řádku – konkrétně před postfix operátorem a pak za continue, break, return a throw. Kupříkladu v kódu

    return
        a + b;

bude automaticky doplněn středník za return a funkce bude místo a+b vracet undefined. Před touto zjevnou chybou vás neuchrání ani důsledné psaní středníků.

Operátor sčítání

O operátoru sčítání + se zmiňujeme, protože má dvojí funkci – sčítání čísel a spojování řetězců. Pokud si nepohlídáme typy hodnot, může nás překvapit 1 + '1' === '11'.

Operátor funguje následovně. Pokud je operand objekt, je nejprve převeden na primitivní typ (viz výše). Dále, pokud je alespoň jeden z operandů řetězec, je druhý převeden také na řetězec a výsledkem je spojení obou řetězců. V ostatních případech jsou operandy převedeny na čísla a sečteny jako čísla. Pak je taky zřejmé, proč [] + [] vrací prázdný řetězec a [] + {} řetězec  '[object Object]'.

Závěr

Během psaní článku jsem narazil na výrazy, jejichž výsledné hodnoty vypadají opravdu záhadně.

    {} //vyhodnoceno jako undefined
    {} + '1' //vyhodnoceno jako 1
    {} + [] //vyhodnoceno jako 0
    {} + {} //vyhodnoceno jako NaN

K objasnění stačí pochopit, že {} může být podle kontextu zparsováno jako příkaz prázdný blok, nebo jako výraz – literál pro objekt. Prázdný blok vrací prázdnou hodnotu, tudíž příkaz {} + '1' je ekvivaletní s +'1', jehož výsledkem je číslo 1. Rozmyslet si ostatní případy zvládne jistě čtenář sám. Pokud bychom chtěli vynutit parsování {} jako výrazu, stačí jej uzavřít do závorek, proto ({} + '1') je vyhodnoceno jako '[object Object]1'.

Porozumění článku si můžete vyzkoušet na této sčítací a porovnávací tabulce.

Zdroje

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 15

Přehled komentářů

Balvan NaN Watman
Balvan Re: NaN Watman
bubak jssss
Petr Moc pekný
Radek Miček Re: Moc pekný
Jan Prachař Re: Moc pekný
brambora Re: "Temná strana" JavaScriptu
tranzitivita Tranzitivita ==
Natix Re: "Temná strana" JavaScriptu
m rtfm
ijacek nejasny ==
Jan Prachař Re: nejasny ==
Tany JSpowa
Tany Re: JSpowa
RadekCZ Re: JSpowa
Zdroj: https://www.zdrojak.cz/?p=3664