JavaScript očima programátora

Najít na internetu článek nebo dokonce seriál, který by se systematicky zabýval JavaScriptem, není nic jednoduchého - převažují články, které ukáží, jak deklarovat proměnné, v lepším případě funkce, poví něco o datových typech a tím to většinou končí. Navíc většina článků je mířena na použití JavaScriptu v rámci prohlížeče. Zřejmě i takovýto nedostatek informací vedl k tomu, že se z JavaScriptu stal jazyk, jenž je obestřen mnoha mýty, legendami a polopravdami, a mezi skutečnými programátory je značně neoblíben a/nebo nepochopen. Proto jsem se rozhodl sepsat tento článek, který představuje JavaScript z pohledu programátora, který je odkojen klasickými programovacími jazyky jako Pascal, C, C++, C# nebo Java. Představuje JavaScript (možná by se spíše slušelo říkat ECMAScript) jako univerzální jazyk, bez jakéhokoliv zaměření na nějakou konkrétní oblast nasazení (např. prohlížeče). Proč? Dnešní intenet je plný rich internet aplikací, jejichž výkonnost je často limitována rychlostí JavaScriptu a proto bylo v posledních letech vynaloženo mnoho úsilí na to, aby JavaScript běhal co nejrychleji. Proto lze očekávat nasazení JavaScriptu i v non-browser úlohách, např. se nabízí použití JavaScriptu na server-side záležitosti - pak by mohla být celá RIA napsána v jednom jediném jazyce.

Úvod

Co je JavaScript?
JavaSript je jedním z dialektů jazyka ECMAScript. Další používané dialekty jsou ActionScript, JScript, InScript nebo QtScript. Ukázky v tomto článku jsou otestovány v JavaScriptu verze 1.8, konkrétně implementaci SpiderMonkey, což je vůbec první implementace JavaScriptu a využívá ji např. Firefox (i když dnes v optimalizované verzi TraceMonkey). Ke SpiderMonkey je také vynikají dokumentace, kterou dopočuji strčit do záložek, pokud to myslíte s JavaScriptem vážně.
JavaScript je dynamický jazyk. Dynamičnost spočívá v tom, že nic nemá pevný typ (jde o slabě typovaný jazyk), objekty mohou během běhu programu měnit svoje atributy a je zde magická funkce eval, která spustí string.
JavaScript je funkcionální jazyk. Funkce lze do sebe libovolně vnořovat, přičemž vnější funkce tvoří uzávěr (closure) vnitřních funkcí.
JavaScript je objektově orientovaný, avšak beztřídní, jazyk - nemá třídy, jen objekty. Pro dědění se často používají tzv. prototypy.
JavaScript neumožňuje explicitní uvolnění paměti - o řízení paměti se stará Garbagge Collector.

Datové typy (tak, jak je lze získat pomocí operátoru typeof):

Kromě funkcí a objektů jsou to všechno primitivní typy, tzn. do funkcí se předávají vždy hodnotou.
Při porovnání hodnot máme dvě možnosti:

Funkce

Definice funkce může vypadat nějak takto:

function secti(a, b) { 
    var c = a + b; // lokalni promenna
    a += c;
    return a + b + c;
}

Jedná se o slabě typovaný dynamický jazyk, takže typy se neuváději, vše se vyhodnocuje až za běhu. Pokud např. a a b budou stringy, bude se funkce chovat jinak než kdybychom použili čísla.
Při volání funkce můžeme použít libovolný počet parametrů. Pokud nějaký definovaný parametr při volání neuvedeme, bude jeho hodnota undefined. Pokud jich bude více, můžeme se k nim dostat přes pole arguments (má položku length a indexuje se klasicky od nuly hranatými závorkami).
Možnost neuvedení parametrů tedy znamená, že všechny parametry jsou volitelné. Když parametrům chceme přiadit nějakou defaultní hodnotu, dělá se to obyčejně takto:

function secti(a, b) {
    a = a || 0; // pokud je a undefined nebo null, priradi se do nej nula
    b = b || 0; 
    var c = a + b; // lokalni promenna
    a += c;
    return a + b + c;
}

Funkce je zároveň objektem a lze ji definovat i takto: new Function("a", "b", "return a + b;")
Poslední parametr je string nesoucí tělo funkce, všechny předchozí parametry jsou názvy parametrů funkce.
Uvnitř funkce můžeme definovat lokální proměnné pomocí klíčového slůvka var, jak bylo ukázáno.

Funkce lze do sebe libovolně vnořovat, přičemž closure tvoří vždy celá funkce, ne blok příkazů uzavřený ve složených závorkách, jak je to běžné např. v C#. Tam bychom napsali kód v následujícím smyslu a vše by fungovalo.

for (var i = 0; i < poleObjektu.length; i++) {   
  var item = poleObjektu[i];
  document.getElementById(item.id).onclick = function() {   
    alert(item.name);   
  }
}

V JavaScriptu toto ale fungovat nebude, protože closure se tvoří vždy jen na úrovni celé funkce, takže to, že item definujeme uvnitř těla cyklu, nám nepomůže.
Od verze JavaScriptu 1.7 má náš problém snadné řešení - místo klíčového slova var použijeme nové klíčové slovo let.
Pokud jsme odkázání pracovat s nižší verzí JavaScriptu, nezbývá nám než zajistit vytvoření další úrovně closure - voláním další funkce v těle cyklu:

function makeAlertFunction(message) {
    return function() { alert(message); }
}
for (var i = 0; i < poleObjektu.length; i++) {   
    var item = poleObjektu[i];
    document.getElementById(item.id).onclick = makeAlertFunction(item.name);
}

Objekty

Objekt je vlastně jen hash table, nese tedy hromadu dvojic klíč - hodnota. Hodnotou může být cokoliv, tedy i funkce. Položka se definuje prostým přiřazením: obj.novaPolozka = 5;. Pokud položka neexistuje, dostáváme při jejím čtení hodnotu undefined. K položkám se dá přistupovat dvěma způsoby:

obj.polozka = 5;
obj["polozka"] = 5;

Položku objektu lze i zrušit: delete obj.polozka;

Nyní bych rád udělal menší vsuvku a zmínil jak vypadá běhové prostředí. Je to děsivě jednoduché - vše je objekt (i primitivní objekty se umí zaboxovat) a vždy se nacházíme v kontextu nějakého objektu (k němuž máme přístup pomocí všudypřítomného readonly this). Pokud jsme na top-level úrovni, ukazuje this na globální objekt (v prohlížečích to samé jako window).

Zpět k objektům. Jak vytvořit nový objekt?

Jen pro úplnost bych dodal, že každý objekt vytvořený pomocí constructor function, obsahuje odkaz na svou constructor function v položce constructor, tedy např. c.constructor.
Následuje komentovaný příklad, na kterém si ukážeme i něco nového, zajímavého a užitečného, takže nepřeskakovat ;-)

// vytvorime promennou Car, do ktere priradime funkci se tremi parametry
// velke pismenko na zacatku jmena funkce indikuje, ze se jedna o constructor function a tudiz by mela byt volana pomoci new
var Car = function(name, model, manufactured) {
    // nadefinujeme dve polozky v aktualnim this objektu
    this.name = name || 'Honda'; // pokud nebylo jmeno specifikovano, pouzije se 'Honda'
    this.model = model || 'Civic';
    
    // jsme ve funkci, takze muzeme nadeklarovat lokalni promennou
    // ta bude zcela podle ocekavani viditelna jen z teto funkce a funkci vnorenych
    // o lokalnich promennych v constructor functions se casto mluvi jako o privatnich polozkach objektu
    // k takovym polozkam se pristupuje jen pres jejich jmeno, tedy "made", zadne "this.made"!
    var made = manufactured;
    
    // nadefinujeme polozku, do niz priradime funkci, tedy vytvorime metodu objektu this
    // uvnitr metody muzeme pouzit lokalni promennou made
    this.howOld = function(now) { return now - made; }
    
    // stejne tak ale vidime i parametry funkce
    // parametry funkce jsou taktez oznacovany za privatni polozky objektu
    // a ze jsme pouzili stejne jmeno metody jako predtim? neva, stara se prepise
    this.howOld = function(now) { return now - manufactured; }
    
    // dve vyse uvedene metody pristupovaly k privatnim polozkam
    // takove metody se nazyvaji privilegovane
    
    // lokalni promenna muze byt jakehokoliv typu, tedy klidne funkce
    // takze toto je privatni metoda
    // k privatnim polozkam pristupujeme primo jejich jmenem
    // neprivatni polozky objektu musime prefixovat "this."
    // polozky jsou vsechno, i funkce, takze volani metody musi byt vzdy necim prefixovano
    // (v pripade tehoz objektu prefixem "this.")
    var printInfo = function () { println(this.name + " from " + made); }
    
    // vyse uvedene se da zapsat zkracene takto (drobne rozdily v implementaci tam ale jsou)
    function printInfo2() { println(this.name + " from " + made); }
    
    // je dobrym zvykem u novych objektu definovat metodu toString, ktera vraci popis objektu
    // toto je public metoda, neni privatni ani privilegovana
    // vsimneme si, ze do polozky toString neprirazuji nejakou anonymni funkci jako v predchozich ukazkach, ale pojmenovanou funkci - to muze pomoci pri debugu pri prochazeni stacku
    this.toString = function toString () { return this.name };
}

// vytvorime Hondu Civic s neznamym datem vyroby
var hc = new Car();
hc.howOld(); // vrati Nan (takovy ciselny undefined)
hc.toString(); // vrati "Honda"
//hc.printInfo(); // skoncilo by chybou - v constructor function nikde nevidim prirazeni do this.printInfo

Pole

Zajímavou a užitečnou třídou je Array, tedy pole, konkrétně dynamicky rostoucí pole. Má položku length, která vrací hodnotu nejvyššího indexu v poli + 1. Pokud přiřazujeme do objektu pole položky s celočíselnými klíči, strkají se do pole. Protože je pole ale zároveň objekt, můžeme do něj přiřazovat i položky s nečíselnými klíči, např. pole.bla = "nazdar";

var pole = []; // to same jako new Array();
var inicializovanePole = [1, "bla", { name: "ja" }, 8, 5.8 ];
pole[2] = "dva"; // pole.length == 3, pole[0] == undefined, pole[1] == undefined
pole["2"] = "dva"; // stejne jako predchozi
pole.push("dalsi"); // dana hodnota se umisti na konec pole
pole[pole.length] = "dalsi"; // lamerska verze predchoziho
var dalsi = pole.pop(); // vrati posledni prvek pole a prvek z pole vynda
pole.length = 1; // nastaveni velikosti pole

this

Klíčové slovíčko this je v JavaScriptu obestřeno mnoha mýty, přitom jeho fungování je docela jednoduché.
Na začátku programu je nastavené na globální objekt a změnit se dá několika způsoby:

Prototypy

Vše je objekt, tedy i funkce je objektem. To je vidět v předcházejícím odstavci - funkce (reprezentovaná objektem třídy Function) má metody call a apply. Dále má ještě metodu toString(), která vrací zdrojový kód funkce (pokud je k dispozici). Objekt Function má dále položku length, která udává počet deklarovaných parametrů, a hlavně má položku prototype. prototype obsahuje položky, které jsou společné pro všechny objekty vytvořené pomocí této funkce.
V praxi to funguje takto: napíšeme třeba var spz = hc.spz;. JavaScript se nejprve podívá, zda má objekt hc nějakou položku s názvem spz. Pokud ano, prostě ji vrátí. Pokud ne, podívá se do objektu prototype, zda ten nemá položku položku spz. Pokud ji ani ten nemá, skončí volání chybou.
Důležitá je zde ta skutečnost, že prototype je pořád jen jeden, bez ohledu na to, kolik objektů jsme pomocí dané funkce vytvořili - všechny mají ten samý prototype. prototype si tedy můžeme představit jako kontejner pro statické položky třídy (constructor function).
Jak je to ale s přiřazením do takové položky? To probíhá jinak, tak bacha na to. Když uděláme hc.spz = 'neco';, tak se JavaScript koukne, jestli existuje v objektu hc položka s názvem spz. Pokud ano, tak je její hodnota přepsána novou hodnotou. Pokud položka není nalezena, je v objektu vytvořena. Tedy při přiřazování se prototype neuplatní!

var hc = new Car();
var sf = new Car("Skoda", "Fabia");

// zajistime, aby vsechny objekty vytvorene pomoci Car mely polozku spz
Car.prototype.spz = 'prvni';
println(hc.spz); // 'prvni'
println(sf.spz); // 'prvni'

// pri prirazeni se prototype neuplatnuje
hc.spz = 'druha';
println(Car.prototype.spz); // 'prvni'
println(hc.spz); // 'druha'
println(sf.spz); // 'prvni'

// samozrejme kdyz priradime do prototype...;)
Car.prototype.spz = 'treti';
println(Car.prototype.spz); // 'treti'
println(hc.spz); // 'druha'
println(sf.spz); // 'treti'

// oba objekty byly zkonstruovany stejnou constructor function
println(hc.constructor == sf.constructor); // true
// a byla to constructor function Car
println(hc.constructor == Car); // true

// zmena prototypu bez explicitniho vypsani jmena constructor function
hc.constructor.prototype.spz = 'ctvrta'; // stejne jako Car.prototype.spz = 'ctvrta';
println(Car.prototype.spz); // 'ctvrta'
println(hc.spz); // 'druha'
println(sf.spz); // 'ctvrta'

// vyse uvedene neplati jen na stringy, ale na vse, tedy i na funkce/metody

Co je vhodné umístit do prototype? Jsou to položky, které se příliš nemění, tzn. konstanty a především metody. Když totiž umístíme metodu do prototype, tak se už při každém volání konstrukční funkce nebude metoda znovu vytvářet a přiřazovat do this, ale místo toho bude existovat jen jednou v prototype - tudíž šetříme pamětí.
Ale pozor, do prototype můžeme dát jen ty metody, které nejsou privilegované, tedy ty, které si nesahají na nějakou lokální proměnnou nebo parametr constructor function. Ono to totiž ani nedává moc smysl.

var Car = function(name) {
    // blbost!
    this.constructor.prototype.testMethod = function() { return name; }
}

Abychom měli přístup k privátním položkám, musí být přiřazení do prototype umístěno v constructor function. To ale znamená, že vytvoření oné metody a přiřazení do prototype probíhá při každém vytvoření instance třídy Car, přičemž se do prototype přiřadí funkce, která ma closure posledního volání funkce Car! Tedy metoda testMethod by vždy vracela jméno poslední vytvořené instance.
Stačí zapamatovat si jednoduché pravidlo - nepřiřazovat do prototype v constructor function, ale až za ní.

Kdyby někdo ale výše popsané chování vyžadoval (což se může stát, člověk nikdy neví), tak bych zde upozornil na jednu věc (vyžaduje ale znalosti z další části článku, takže tuto poznámku zatím klidně přeskočte). Privilegované položky v prototype totiž nejsou enumerabilní, tj. pokud budeme projíždět všechny položky objektu cyklem for-in, nedostaneme se k privilegované metodě. A to může způsobit problémy při implementaci vícenásobné dědičnosti. Abychom se tomuto problému vyhli, nepřiřazujeme v ukázce do Car.prototype, ale do this.constructor.prototype, což bude např. v případě volání konstruktoru z poděděné třídy BestCar znamenat totéž co BestCar.prototype. Použitím konstrukce this.constructor.prototype tedy umožníme přiřazení do prototype aktuálního objektu a vyhneme se nutnosti vyenumerovat tuto položku při kopírování z prototype do prototype.

Properties

Jak jsem si již řekli, položky objektu jdou definovat prostým přiřazením, tedy obj.polozka = hodnota; či obj["polozka"] = hodnota;. Je tu ale ještě jedna možnost - můžeme definovat properties, tedy položky, které mají funkci pro čtení hodnoty (getter) a pro zápis hodnoty (setter). Jedna z těchto funkcí může být vynechána, pak mluvíme o read-only, resp. write-only property.

// definice na existujicim objektu
obj.__defineGetter__("prop", function() { return this.propValue; } );
obj.__defineSetter__("prop", function(value) { this.propValue = value; } );

// definice v ramci object initializeru
var obj2 = { get name() { return this._bla; }, set name(value) { this._bla = value; } };

// pouziti
var hp = obj.prop;
obj.prop = 'bla';

Pokud chceme zjistit, zda existuje getter nebo setter pro dané jméno, můžeme použít funkce __lookupGetter__ a __lookupSetter__.

A když jsme už u těch podtržítkových záležitostí, tak přihodím jednu třešňičku, kterou umožňuje SpiderMonkey. Do obj.__noSuchMethod__ můžeme přiřadit funkci, která očekává dva parametry. První je jméno funkce a druhý její parametry. Tato přiřazená funkce bude zavolána vždy, když někdo na objektu obj zavolá metodu, která není definována.

K properties bych ještě dodal, že jsou to de facto metody, takže pokud přistupují jen k public položkám (tj. přes this), patří do prototype.

Statements

Teď si dáme trošku oddech a mrkneme na statements. Ty jsou téměř stejné jako v C, takže je netřeba moc rozebírat - máme tu for, while, do-while, break, continue, if, if-else, switch...
Za zmínku snad stojí jen for-in. Ten nám totiž umožní iterovat přes všechny jména položek objektu:

for(var key in someObj) {
    println(key + ': ' + someObj[key]);
}

Pokud chceme iterovat přímo přes hodnoty položek objektu, můžeme přes for-each:

for each (var v in someObj) {
    println(v);
}

Dědičnost

A to nejlepší nakonec - dědičnost není JavaScriptem přímo podporována, avšak v jazyku jsou připravené takové konstrukty, které nám umožní dědičnost implementovat. Je tedy více možností, jak dosáhnout kýženého cíle. Zde bych rád ukázal některé používané přístupy a přidám ještě nějaké svoje nápady.
V následujících příkladech budu předpokládat, že T1 je bázová třída a T2 je třída odvozená od T1.

Často je k vidění následující konstrukce: T2.prototype = new T1;
Toto velmi jednoduché řešení jakž-takž funguje, ale má řadu nevýhod:

Výhodou tohoto řešení je jednoduchost zápisu.

Následující řešení se mi zdá zajímavější:

var T2 = function(cpar1, cpar2, cpar3) {
    // vytvorime public polozku s nazvem parentClass, ktera ukazuje na constructor function predka
    // tim dame do T2 metodu, ktera umi zkonstruovat objekt typu T1
    this.parentClass = T1;
    // zde udelame to, pred cim jsem varoval - zavolame constructor function bez "new"
    // protoze se ale jedna o volani metody objektu T2, preda se do ni aktualni this
    // takze tato trida bude zinicializovana jako T1
    this.parentClass(cpar1, cpar2 + cpar3);
    // nyni nasleduji T2 specific zalezitosti jako obvykle
}

Výhody:

Nevýhody:

U posledních dvou nevýhod jsem sám vymyslel (heč! :)) jak se jich zbavit. Jednoduše využijeme jiné možnosti, jak protlačit vlastní this do funkce.

var T2 = function(cpar1, cpar2, cpar3) {
    // zavolame constructor function T1, ale podstrcime mu aktualni this
    T1.call(this, cpar1, cpar2 + cpar3);
    // pokud ma T1 a T2 stejne parametry, lze zapis zkratit nasledovne
    T1.apply(this, arguments);
    // nyni nasleduji T2 specific zalezitosti jako obvykle
}

Stále nám zde ale zůstává problém s neděděním prototype. Zde ukážu dva přístupy k řešení problému.

Finální komentovaná ukázka dědičnosti

Object.prototype.extend = function extend(b) {
    b = b.prototype;
    for (var i in b) {
        var g = b.__lookupGetter__(i), s = b.__lookupSetter__(i);
        if (g || s) {
            if (g) this.prototype.__defineGetter__(i, g);
            if (s) this.prototype.__defineSetter__(i, s);
        } else {
            this.prototype[i] = b[i];
        }
    }
    return this;
};
    
var Trida1 = function(cpar1, cpar2) {
    // instancni polozky
    this.field1 = cpar1 + cpar2;
    this.field2 = "hola";
    // privatni polozky
    var pfield1 = cpar2;
    var pfield2 = "hola hej";
    // privatni metoda
    var pmethod1 = function(par1) { println("Trida1.pmethod1 called"); };
    // metoda pristupujici k privatnim polozkam, tedy privilegovana
    this.method1 = function(par1) { println("Trida1.method1 called"); this.method2(); println(pfield1); };
    // property pristupujici k privatnim polozkam    
    this.__defineGetter__("prop1", function() { return pfield1; } );
    println("Trida1 contructor called");
};
// metoda (a prip. property) pracujici pouze nad public polozkami
Trida1.prototype.method2 = function(par1) { aswprintln("Trida1.method2 called"); };
// konstanty
Trida1.prototype.CONST1 = 1;
    
// podedime tridu
var Trida2 = function(cpar1, cpar2, cpar3) {
    // volame konstruktor predka se dvema parametry
    Trida1.call(this, cpar1, cpar2 + cpar3);
    // instancni polozka
    this.newfield1 = cpar1;
    // privatni polozka stejneho jmena jako privatni polozka v predkovi
    // diky ruznym closures zadny problem
    var pfield1 = cpar2;
    // property, ktera nam zpristupnuje privatni polozky pfield1 teto tridy
    this.__defineGetter__("prop2", function() { return pfield1; } );
    println("Trida2 contructor called");
};
    
//Trida2.extend(Trida1); // kdybychom chteli vicenasobnou dedicnost, pouzijeme tento radek misto dalsich dvou
Trida2.prototype = Trida1.prototype;
Trida2.prototype.constructor = Trida2;


// tridy nadefinovany, nyni je jdeme pouzivat
var t1  = new Trida1(58, 12);
var t2  = new Trida2(-28, -98, -5);
var t22 = new Trida2(59, 65, 158);
t1.method1();
t2.method1();
// cteni privatnich polozek stejneho jmena
println(t22.prop1);
println(t22.prop2);

Singleton

Jako bonus na závěr bych ukázal dvě možnosti, jak implementovat návrhový vzor singleton.

// nadeklarujeme funkci, kterou hned zavolame
var Singleton1 = function() {
    // datove polozky musi byt jako lokalni promenne
    // prirazeni do this by znamenalo prirazeni do aktualniho objektu, coz muze byt ledasco
    var val1 = 1;
    var val2 = 2;
    var val3 = 3;
       
    // z funkce vracime anonymni objekt, ktery tvori verejny interface pro nas singleton
    return {
        prop1 : 2,
        funkce : function(par1, par2) {
            return par1 + par2;
        },
        get value1() { return val1; },
    }
}();

Udělali jsme to, že jsme nadeklarovali funkci, ale nijak jsme ji nepojmenovali ani do ničeho nepřiřadili, ale hned po definici jsme ji zavolali. Funkce vrací anonymní objekt, který tvoří veřejný interface pro náš singleton a pouze přes něj můžeme přistupovat k privátním položkám singletonu. Ty jsou implementovány jako lokální proměnné oné nepojmenované funkce.

// nadeklarujeme funkci, kterou hned zavolame pomoci new
var Singleton2 = new function() {
    // datove polozky mohou byt privatni i public
    this.val1 = 1;
    var val2 = 2;
    var val3 = 3;
       
    this.prop1 = 2;
    this.funkce = function(par1, par2) {
        return par1 + par2;
    }
    this.__defineGetter__("value1", function() { return val1; });
};

Toto řešení funguje tak, že nadeklarujeme constructor function a hned ji zavoláme pomocí new. Zde je úroveň "zatajení implementace" o něco nižší, neboť pomocí Singleton2.constructor se dostaneme k oné nepojmenované constructor function.

Shrnutí

Augi