Přejít k navigační liště

Zdroják » JavaScript » „Temná strana“ JavaScriptu

„Temná strana“ JavaScriptu

Články JavaScript

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

Komentáře

Subscribe
Upozornit na
guest
15 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
Balvan

Nieco aj ohladom JS ale vtipnejsie :-)
https://www.destroyallsoftware.com/talks/wat

Balvan

Pardon , link je aj v clanku
je noc a uz nevnimam poriadne

bubak

Me zajima kdo bude uklizet tohle svinstvo za par let.

Petr

Výborný článek, jako člověk co už pár let JavaScript dělá jsem nevěděl jestli se smát nebo plakat… Nakonec zvítězil smích a shledávám tento článek vtipnější než komixy na rootu. Navíc pěkné ucelené shrnutí. Díky!

Radek Miček
brambora

Something, something, something, dark side. Something, something, something, javascript.

tranzitivita

[] == 0. Check. 0 == ‚0‘. Check. ‚0‘ != []. Ch- wait, what?!

Vygenerovana tabulka pro operator == :

http://zero.milosz.ca/

Natix
m

člověk si k tomu nepřečte manuál a pak se diví..

KarelI

Neni mi v sekci 2 u operatoru == jasne nasledujici :
‚2‘ == false
‚true‘ == false
‚false‘ == false

Dle pravidel by melo dojit k prevodu na cisla, skutecne k tomu dochazi a tedy by mely vysledky byt 2 == 0, NaN == 0 a NaN == 0, vse nerovnosti. Pokud tedy autor mel na mysli, ze vsechny vyrazy jsou pravdive, pak je v clanku chyba. Pokud je to jinak tak mi to prosim vysvetlete. Dekuji :-)

Tany

Zamyšlení patří k V8 Engine a z něho stoupající nodeJS.

JS je jiný, také jsem na něj nadával, nesnášel ho. Pak jsem v něm objevil zlatý grál programovaní, pište to co myslíte, jak to myslíte. (Lehko se řekne, pár let přemýšlení v php,c++ .. se těžko „vypne“)

Noční můra programátora je to, že 15tiletá holka se naučí js imho o hodně rychleji než člověk zvyklý na ifdef hell. JS má takový, řekněme přirozenější přístup k jádru věci.

Kup housky, nebo rohlíky .. nakup.push(kram[‚­housky‘] || kram[‚rohliky‘]) .. to je celkem intuitivní ne ?

Vše je objekt, tak je to i v reálném světě ne ? Tak proč ne… undefined je instance objektu v globalnim prostoru, ale tak je to podle mě přirozené, ne ?
Prodejte mi „bla“ .. to nevedeme

JS poskytuje spoustu nebezpečných, možná i klasického pohledu nelogických možností, ale tam je to zase o tom, kdo to píše .. prasit se dá vše.

Stačí se třeba podívat na ten humus mootools, to je správná cesta do pekel. Extendovat prototypy základních objektů, humus totální. Nebo třeba totálně „krásné“ věšení eventů $(‚.blah‘).li­ve(event,cb{e}), kde si člověk automaticky zaprasí celý document.

Tany

věšení eventů v jQuery ..

RadekCZ

Co máš přesně proti rozšiřování prototypů základních objektů? Pokud nerozšiřuješ přímo Object.prototype (což by teda dělalo neplechu, bylo by to na úplně všech objektech), tak to podle mě nic špatného není. Občas se mi prostě hodí moje vlastní metoda na polích, funkcích, řetězcích atp.

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.