Jak správně debugovat JavaScript?

Debugovat JavaScript většinou není problém – spustíte Nástroje pro vývojáře, Inspektor, Firebug, F12, apod. a nastavíte break-point (nebo prostě do kódu napíšete „debugger„).

Někdy ale narazíte na speciální situace, kdy není možné debugger použít (např. na mobilu, chytré televizi, apod.) nebo se debugger nechová přesně tak, jak byste očekávali.

Poznámka: následující ukázky kódu se mohou zdát nelogické, špatně navržené či dokonce chybné – ale o to přeci jde. Kdyby byl každý kód dokonale navržený a bezchybný, nebylo by ho potřeba debugovat!

Metody debugování

  1. Nejčastějším způsobem debugování javascriptu je již zmíněné vytvoření break-pointu nebo příkazu debugger, které je nejvhodnější, pokud potřebujete kód krokovat nebo se podívat na obsah nějakých proměnných v daném okamžiku.
  2. Další možností je zápis do konzole, které se hodí, pokud přímo nepotřebujete kód zastavit (nebo je to přímo nežádoucí), ale potřebujete se dozvědět o nějaké události nebo o obsahu nějaké proměnné. Moderní RIA (Rich Internet Applications) mohou do logu konzole zapisovat stejně často jako desktopové programy nebo mobilní aplikace.
  3. Třetí často používanou metodou je zobrazení alertu, které se dost podobá „brute force“ a používá se v okamžiku, kdy všechny ostatní metody selhávají a vy se potřebujete dozvědět, že došlo k nějaké události (nebo naopak nedošlo v důsledku v chyby v kódu).

Alerty vs. Eventy

Prvním problémovým případem je debugování handlerů (observerů, listenerů) událostí.

$('input').on('click', function() {
    alert('click on input');
});
$('form').on('click', function() {
    alert('click on form');
});

Zdánlivě univerzální kód, který se hodí na otestování kliků na formuláři (např. pokud testujete web pro chytré televize, které se ovládají dálkovým ovládáním se šipkami). Očekávané chování je, že když kliknete na input, vyskočí alert „click on input“ a po jeho zavření vyskočí „click on form“ díky tomu, že událost Click probublá nahoru.

Problém ale nastane v okamžiku, kdy skutečně kliknete… a vyskočí jen jeden alert. Chyba je v tom, jak prohlížeče zpracovávají volání metody alert(). Když totiž napíšete dva alerty za sebe, tak se zobrazí po sobě, protože vykonávání kódu se během zobrazení alertu pozastaví. Zastaví se ale jen kód dané funkce, nikoliv ale vyvolávání událostí a jejich handlerů. Takže zatímco je zobrazen první alert, událost dál probublává a dostane se k druhému handleru, který již není pozastaven a tak se vyvolá – a znovu zavolá metodu alert(). Zde se může chování prohlížečů lišit – některé druhé zavolání ignorují, jiné naopak nahradí obsah alertu (zobrazenou zprávu).

Stejný problém je většinou i s break-pointy a příkazy debugger, protože ty pozastavují kód stejně jako alert() a tudíž události mohou i nadále běžet. Stejně tak mohou běžet i časovače (setTimeout() a setInterval(), protože i ty vnitřně pracují s událostmi).

Pokud tedy chcete testovat sled událostí (eventů), je potřeba si zprávy ukládat do konzole:

console = console || {log:function(){}};
$('input').on('click', function() {
   console.log('click on input');
});
$('form').on('click', function() {
    console.log('click on form');
});

Nebo, pokud není dostupná (např. u mobilního telefonu nebo chytré televize), je potřeba vymyslet jinou, konzoly podobnou, metodu – např. zapisovat zprávy do nějakého DIVu na konci stránky, ukládat do cache, jejíž obsah zobrazíte s časovým zpožděním, odesílat zprávy AJAXem do databáze apod.

Zde uvedený příklad bude (na zařízeních, kde konzole není k dispozici), ukládat volání metody console.log() do cache a zobrazí její obsah 2 sekundy po posledním zavolání.

console = console || {
    _cache: [],
    _delay: null,
    log: function(message) {
        //ulož zprávu do cache
        console._cache.push(message);
        //zruš poslední volání setTimeout
        if (console._delay) {
            clearTimeout(console._delay);
        }
        //nastav zobrazení cache za 2 sekundy
        console._delay = setTimeout(
            function() {
                alert(console._cache.join('\n'));
                console._cache = []; //clear
            },
            2000
        );
    }
}

Pořadí v konzoli

Dalším problémem, úzce souvisejícím s předchozím případem, je pořadí zobrazování zpráv v konzoly – i když k ní může docházet jen výjimečně nebo jen v některých prohlížečích.

K této chybě často docházelo u starých verzí Firebugu, nové prohlížeče a debuggery již mohou být proti této chybě ošetřeny. Nicméně je potřeba na ni myslet a nespoléhat se na pořadí zpráv v konzoly.

for (i = 0; i < 100000; i++) {
    if (i%33 === 0) { console.warn(i); }
    else if (i%100 === 0) { console.error(i); }
    else { console.log(i); }
}

Pokud je prohlížeč vytížen zpracováním javascriptu a zároveň je zahlcen zprávami, které má do konzole zapsat, pozdrží zápis dalších zpráv a uloží je jen do cache a do konzole je zapíše později, až na to bude mít víc času.

Když pak použijete pro zápis do konzole různé metody (log, warn, error), může dojít k tomu, že se následně vypíší do konzole v jiném pořadí, protože debugger nejdříve vypíše všechny chyby, pak varování atd. Někdy k stejnému problému může dojít, i když používáte stejnou metodu, ale volanou z různých míst!

Pokud skutečně potřebujete zjistit pořadí volání některých funkcí, je potřeba použít alerty, které kód pozastaví, nebo (pokud jde o eventy), použít již výše uvedený trik s cachováním zpráv a zpožděným zobrazením.

Přepisování hodnot debuggerem

Většina JS debuggerů má i funkci Watch, díky které můžete okamžitě zjistit obsah proměnných a dokonce i výsledků volání funkcí.

function process(x) {
    return x*x/2;
}
//watch: process(i);
var i = 0; //process(i) == 0
i = 1;     //process(i) == 0.5
i *= 10;   //process(i) == 50
i += 'kg'  //process(i) == NaN

Pokud si při debugování nastavíte sledování hodnoty funkce, debugger automaticky po každém kroku zavolá tuto funkci, aby zjistil její aktuální výsledek.

Problém tak nastane v okamžiku, kdy jde o složitější (tzv. nereentrantní) funkci, která si pamatuje poslední stav a vychází z něho) nebo přímo ovlivňuje stav svého okolí.

var i;
function process(x) {
    i = x*i/2;
    return i;
}
//watch: process(i);
var i = 0;
i = 1;
i *= 10;
i += 'kg'

Pokud budete u tohoto kódu sledovat výsledek funkce process(i), bude docházet k tomu, že po každém kroku (přiřazení do i) se zavolá funkce process(), která do i nastaví jinou hodnotu a pak tedy debugovaný kód bude vracet jiný výsledek, než když ho spustíte přímo.

Jednoduché řešení neexistuje, kromě toho, že nebudete takové metody psát nebo je nebudete debugovat.

Debugování stavu objektu

Na předchozí případ navazuje další a tím je sledování stavu objektu při debugování.

Pokud si totiž do sledování nastavíte přímo objekt, debugger po každém kroku zavolá jeho metodu toString(), aby zjistil, jak se chce daný objekt v daném okamžiku prezentovat.

Pokud ale metoda toString() nějak mění obsah samotného objektu, tak to může mít v průběhu debugování vliv na výsledek kódu:

var weight = function(unit) {
    this.value = '0' + unit;
    this.unit = unit;

    this.toString = function() {
        if ('string' !== typeof this.value) {
            this.value = this.value + this.unit;
        }
        return this.value;
    };
    this.load = function(weight) {
        this.value = parseInt(this.value, 10);
        this.value += weight;
        return this.toString();
    }
    this.unload = function(weight) {
        this.value = parseInt(this.value, 10);
        this.value -= weight;
        return this.toString();
    }
}
//execution code
var truck = weight('kg');
truck.load(1000); //truck.value === '1000kg'
//...
truck.unload(500); //truck.value === '500kg'

Pokud v tomto kódu budete debugovat metody load(), dojde k tomu, že v prvním kroku se hodnota value převede z řetězce na číslo… avšak debugger okamžitě zavolá toString() a číslo opět převede na řetězec. Další příkaz pak nebude provádět číselné sčítání, ale spojování řetězců (protože obě funkce používají operátor „+„). Výsledek metody load() tedy bude při debugování ‚0kg1000kg‚. Tento případ se dá ještě snadno odhalit, ale u metody unload() by při debugování byl výsledek ‚NaNkg‚, což už může být nepochopitelné (protože operátor „“ pro řetězce nefunguje a vrací chybu Not-a-Number; metoda toString() pak hodnotu převede na řetězec ‚NaN‚ a připojí jednotky).

Poučení zní: v systémových metodách jako jsou toString(), valueOf() apod. nikdy neměňte stav objektu – tyto metody totiž mohou být volány kdykoliv i bez vašeho vědomí a mohou stav objekt poškodit.

Pokud už potřebujete výsledek těchto metod cachovat, zajistěte si nějaký nástroj, jak cachování během debugování vypnout (např. nastavením globální hodnoty console.isDebugging, kterou pak můžete měnit v konzoli během krokování).

Podmíněné pozastavení

Máte nějaký kód, který běží vcelku spolehlivě, ale občas se stane, že selže – zpravidla na tom, že nějaká hodnota je nečekaně NULL, undefined nebo má jiný typ (např. řetězec místo čísla, jak ukázal předchozí případ).

První, co vás asi napadne, je do místa, kde dochází k chybě vložit break-point a čekat, až k chybě dojde. Problém je ale v tom, že break-point zastaví program vždy, i když je požadovaná hodnota správná.

Druhá možnost tedy je využít programovou podmínku:

value = 1;
//...
if (value === null) { debugger; }
process(value);

Ta funguje dobře, ale vyžaduje úpravu samotného kódu – a pokud k chybě dochází např. v jQuery knihovně načítané z CDN, tak taková možnost nepřichází v úvahu.

Většina současných debuggerů s tímhle ale počítá, a umožňuje vám opodmínkovat i break-point. Zpravidla je to možné tak, že do kódu vložíte break-point a následně na něj kliknete pravým tlačítkem. Tím zobrazíte nějaké vstupní pole, kam můžete zadat podmínku. V předchozím případě by pak stačilo dát break-point na příkaz process(value) a do podmínky zadat value === null. Debugger pak automaticky před každým zastavením na daném příkazu vyhodnotí podmínku, a pokud není splněna, příkaz provede – a pokud splněna je, před jeho vykováním kód zastaví a umožní vám podívat se na stav proměnných nebo si daný příkaz odkrokovat.

Obdobně většina debuggerů umožňuje break-point vypnout, ale zároveň ho zachovat na místě. To se hodí, pokud potřebujete debugovat nějakou funkci, kde dochází k chybě po určitě akci uživatele, ale zároveň se metoda volá několikrát před tím (během načítání stránky). Zpravidla stačí kliknout na break-point se shiftem (tedy SHIFT+LMB) a break-point se vypne. Stejným způsobem se pak i zapne. U jiných debuggerů může být potřeba jít do sekce Breakpoints a odškrtnout checkbox u daného break-pointu.

Hledání chyby alertem

Tato metoda je známa především z dob prvních iPhonů a iPadů, které nebylo možno debugovat a přitom jejich javascript interpreter obsahoval řadu chyb. Ty se pak projevovali tak, že se vždy provedla jen část funkce a pak náhle (kvůli nějaké chybě) skončila. Podobná situace nastává u debuggerů v IE7 a IE8, které v některých případech nedokáží správně určit příkaz, na kterém došlo k chybě a odkazují vás do zcela jiným míst vašeho skriptu (nebo dokonce mimo něj).

Metoda alertu spočívá v tom zapsat do kódu mezi příkazy alerty a pak počítat, kolik se jich zobrazilo:

    alert('start');
prepare();
    alert(1);
load();
    alert(2);
process();
    alert(3);
finish();
    alert('end');

Pokud takový kód spustíte, a zobrazí se jen alerty ‚start‚, ‚1‚ a ‚2‚, můžete s pravděpodobností limitně blížící se jistotě tvrdit, že uvnitř metody process() je nějaká chyba, která způsobí ukončení vykonávání skriptu. Následně pak stejný postup použijete i uvnitř problematické metody, až nakonec najdete konkrétní příkaz, který se prohlížeči nelíbí.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Tato stránka používá Akismet k omezení spamu. Podívejte se, jak vaše data z komentářů zpracováváme..