JavaScript očima programátora

Pozn.: V původním článku se vyskytovaly některé nepřesnosti, které mě vedly k revizi článku, který je ke shlédnutí zde. Z historických důvodů zde nechávám i tuto původní verzi.
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):

  • number – číslo celé (117, 015 (oktalově), 0x15 (hexa)) i desetinné (3.14, 3.14E-15)
  • string – textový řetězec, „bla“ nebo ‚bla‘
  • booleantrue nebo false
  • undefined – má jedinou hodnotu undefined
  • function – funkce
  • object – objekt

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:

  • Buď můžeme použít klasické != a ==, které v případě neshodnosti typů provede potřebné konverze. Takže např. 3 == „3“ je true.
  • Druhou možností je použití striktního porovnání !== a ===, které neprovádí žádné konverze, takže 3 === „3“ je false.

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?

  • Anonymní objekt snadno – do složených závorek uzavřeme dvojice jmeno : hodnota a oddělíme je čárkami. Hodnota může být cokoliv, klidně další objekt nebo funkce (v tomto případě spíš metoda):
    var car = { 
        name : "Honda", 
        model : "Civic", 
        owner : { name : "Jiri", surname : "Novak" }, 
        printMe : function() { 
            return this.name + ' ' + this.model + ' owned by ' + this.owner.name + ' ' + this.owner.surname; 
        }, 
    };

    Samozřejme můžeme vytvořit i prázdný objekt: var empty = {};
    Takovému způsobu zápisu objektu do složených závorek se říká object initializer a zápis je stejný jako Jsonu.
    Pokud voláme funkci objektu (tedy metodu) car.printMe();, je před voláním metody do this přiřazeno car. Proto se metoda chová dle očekávání.

  • Voláním funkce prefixované klíčovým slovem new – takové funkce je slušné pojmenovat s prvním písmenem velkým a říkat jim constructor functions. Klíčovým slovem new řekneme, že chce vytvořit nový (prázdný) objekt a tento použít jako this uvnitř volání funkce. Uvnitř funkce máme tedy k dispozici this, což je úplně prázdný objekt. Logickým cílem funkce je tento objekt zinicializovat.
    function Car(carName, model) {
        this.name = carName;
        this.model = model;
        this.printMe = function() { 
            return this.name + ' ' + this.model; 
        };
    }

    Na constructor functions tudíž můžeme pohlížet podobně jako na třídy v jiných jazycích – je to totiž předpis, jak zkonstruovat konkrétní objekt. Pokud budu v dále textu mluvit o třídách, budu mluvit o constructor functions.

    Pokud zavoláme toto, vytvoří se nám objekt podle očekávání: var c = new Car(„Honda“, „Civic“);. Co když ale na slovíčko new zapomeneme? Pak nedojde k vytvoření nového prázdného objektu a jeho přiřazení do this. Bez new se jedná o úplně normální volání funkce a tudíž uvnitř funkce dojde k přiřazením do aktuálního this objektu. Pokud se nám podaří takovou funkci zavolat bez new z top-level úrovně, nadefinujeme položky name a model na globálním objektu.

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:

  • Zavoláním funkce s klíčovým slovem new. Jak už bylo řečeno, v tomto případě dojde k vytvoření nového prázdného objektu a jeho přiřazení do this, tedy něco jako this = {};. Po návratu z funkce je this vráceno na původní hodnotu.
  • Volání metody. Viz. příklad výše – hc.howOld();. Dojde k nastavení thisu na hc, zavolání metody a obnovení hodnoty this.
  • Volání metody pomocí call. Metodu můžeme zavolat takto: hc.howOld.call(someOtherObject, par1, par2);. Zde se stane něco podobného jako v předchozím případě, pouze this je nastaveno na námi zadanou hodnotu someOtherObject.
  • Stejně jako předchozí případ funguje apply. Rozdíl je v tom, že nyní nepředáváme parametry jednotlivě, ale apply má dva parametry – první parametr je nová hodnota thisu, druhá parametr je pole parametrů. Metoda apply se využívá např. tehdy, když chceme z funkce zavolat funkci se stejnými parametry jako má aktuální funkce – pak zavoláme funkce.apply(this, arguments); Jak bylo řečeno, arguments obsahuje pole aktuálních parametrů, takže tímto ho pouze přepošleme do jiné funkce.

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:

  • Při této deklaraci dojde k vytvoření instance třídy T1, což nemusí být úplně ok (kdyby třeba T1 alokovala nějaké zdroje apod.).
  • T2.prototype.constructor bude nelogicky roven T1. Lze řešit přiřazením T2.prototype.constructor = T2;
  • T2 nezdědí z T1 privátní položky, takže volání privilegovaných metod selže.
  • Při vytváření instance T2 se nevolá konstuktor třídy (constructor function) T1.
  • V této podobě neumožňuje vícenásobnou dědičnost.

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:

  • Informace o dědění je obsažena přímo v definici třídy.
  • Můžeme volat konstruktor předka s libovolnými parametry.
  • Privátní položky T1 jsou správně zinicializovány a neperou se s položkami T2, neboť každé žijí v jiné closure.
  • Nejsme omezeni na jednoho předka – můžeme volat více constructor functions.

Nevýhody:

  • Nedědí se prototype.
  • Zápis dědičnosti jsou dva příkazy.
  • Zavádí se nová public položka objektu (v ukázce pojmenovaná parentClass).

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.

  • Cílem je, aby T2.prototype obsahovalo to samé, co T1.prototype, plus něco navíc. Řešení je tudíž naprosto přímočaré:
    T2.prototype = T1.prototype;
    // obnoveni konstruktoru
    T2.prototype.constructor = T2;
    // T2 specific prototype polozky

    Nevýhodou tohoto přístupu je to, že neumožňuje vícenásobnou dědičnost. Té se ale v praxi já osobně snažím vyhnout, protože to přináší další zlo, na které je třeba pamatovat – možné překrývání jmen položek jednotlivých bázových tříd.

  • Pokud chceme přesto umožnit vícenásobnou dědičnost, postupoval bych následovně. Nejprve si do všech objektů přidáme metodu extend, která nám překopíruje všechny (resp. všechny enumerabilní) položky daného prototype do prototype aktuálního objektu.
    // metoda na kopirovani z prototypu do prototypu
    // pokud nechceme vicenasobnou dedicnost, tak ji nepotrebujeme
    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];
            }
        }
    };

    Místo přiřazení do prototypu pak stačí zavolat T2.extend(T1);

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í

  • JavaScript je dynamický objektově orientovaný skriptovací jazyk
  • všechno je objekt, včetně funkcí
  • objekt je hash mapa (jméno -> hodnota)
  • objekt můžeme vytvořit dvěma způsoby – zavoláním new Funkce nebo pomocí object initializeru: { name: „Ja“, age : 26, } (jako Json)
  • nějaký objekt je vždy aktuální – this (na top-level úrovni je to globální objekt)
  • aktuální objekt (this) můžeme změnit několika způsoby – zavoláním funkce na objektu (tedy metody), voláním new Funkce a dále pomocí speciálních metod třídy Function: call a apply
  • každá funkce má přiřazen prototype, který je sdílen všemi objekty, které byly zinicializovány pomocí této funkce
  • prototype se používá na definování konstant a public metod
  • co bylo definované jako poslední (stejného jména), to platí
  • tento článek nepostihuje všechny možnosti JavaScriptu, ty jsou daleko širší – např. popis všech možností klíčového slova let by vydalo na další článek takovéhoto rozsahu
  • design mého blogu není moc přívětivý na dlouhé výpisy kódů, proto si můžete článek prohlédnout také v plain html zde

Pozn.: S odstupem času a po rytí Daniela Steigerwalda musím uznat, že některé uvedené techniky pro docílení dědičnosti nejsou ideální, některé dokonce nepěkné 😉 Pro další (lepší) techniky, jak implementovat OOP v Javascriptu, doporučuji Danielův seriál na serveru Zdroják.

31 thoughts on “JavaScript očima programátora

  1. Vyborny clanek, diky. Vyborny i pro lidi, co si mysli, ze trestat vyvojare je mozne i civilizovanejsim zpusobem, nez je dlouhodobe vyhnanstvi v JS svete, daleko od C# a C++ 😉

    To se mi líbí

  2. Jako rychlý úvod dobré (pokud nevím co je javascript a zítra mám deadline projektu v javascriptu), ale řada věcí je příliš zjednodušena. Článek by měl být důkladně ozdrojován, aby hloubavější jedinci mohli přejít na lepší četbu.

    To se mi líbí

  3. Aleš: Vývojář je takový lepší programátor, ne? 🙂
    Martin: Tak nějak jsem ten článek myslel 🙂
    Ohledně zdrojů…z vlastní zkušenosti ale vím, že je docela problém sehnat kvalitní zdroje. Proto jsem považoval odkaz na stránku o ECMAScriptu na Wikipedii (kde je odkaz na specifikaci) a hlavně odkaz na výbornou dokumentaci ke SpiderMonkey, za dostatečné ozdrojování.
    Jinak lze na internetu samozřejmě nalézt i super články o JavaScriptu, ale nic vyloženě uceleného. Pokud máte nějaký zajímavý zdroj, podělte se! 🙂

    To se mi líbí

  4. „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.“

    Tenhle argument jsem slysel uz tolikrat, ale proboha no a co 🙂 ? Neni to uplne jedno? Syntaxi jazyka se ucis 5 minut, x-krat vice casu stravis s API, ktere samozrejme bude uplne jine pro system a pro web. (I kdyz pripoustim ze bych hrozne rad psal systemove veci/server side kod (take) v JS, je to jeden z nejlepsich jazyku co znam, uplna parada.)

    BTW kdyz zminujes this, stalo by za to zminit self. Sam si nejsem jist jak by self fungoval mimo prohlizec, abych rekl pravdu.

    Kazdopadne velmi kvalitni a netradicni clanek, jdes do RSS, z toho uz se nevykecas 😉

    To se mi líbí

  5. Ano, ten argument možná trošku podivný a určitě máš pravdu v tom, že často více času zabere naučení API než jazyka.

    self jsem nezmiňoval proto, že jsem chtěl popsat JavaScript jako obecný jazyk, ideálně úplně bez návaznosti na prohlížeče. self je totiž jen properta objektu window, což je DOM objekt, což je specifikum prohlížečového nasazení…

    To se mi líbí

  6. Augii jsi slavny 🙂 doufam ze na me ani v teto fazi zivota nezapomenes 😉 ale jinak jo, docetla jsem, a ten lame serial co vysel na zdrojaku se muze jit zahrabat…

    To se mi líbí

  7. Celý blog jsem přečetl jedním dechem, výborné! A tento článek považuju za asi nejlepší úvod do JavaScriptu, jaký jsem kdy četl. John Resig se svou ubohou knihou by se mohl učit 😉

    To se mi líbí

  8. Ahoj,
    díky za vynikající článek o JS, první, kde se mi podařilo pochopit prototype 🙂
    Doufám, že se tu objeví i další články o JS.
    Shulík

    To se mi líbí

  9. Mně taky chyběly články o tvz. nových věcech v Javascriptu. proto jsemse rozhodl psát také články o Javascriptu, nyní tam mám už 28 článků o Javascriptu, nyní tam mám rozepsaný článek o vlastnostech prototype, již čtvrtý díl se tam nachází. Můžete si je přečíst na: http://programovani.blog.zive.cz/category/javascript/

    Jinak tvůj článek mě velice zaujal. Jen víc takových článků.

    To se mi líbí

  10. Augi, článek je pěkně napsaný, někde možná zbytečně složitý. Ale hlavně! Prezentuje totálně špatné techniky, promiň!

    Nikdy nerozšiřujte Object.prototype!
    Tady máte příklad co se stane http://jsfiddle.net/UpFNH/
    Přestanou fungovat všechny for in cykly.

    Dědičnost nenasimuluješ kopírováním properties.

    Vice v mém článku na zdrojáku (právě píšu)

    tvoje implementace dědičnosti není správná. Kopírování properties prototypu není náhrada dědičnosti. Nebo alespoň ne tak, jak to javascript umožňuje. Na tomhle to selže:
    var Person = function() {};
    var joe = new Person;
    Person.prototype.someFn =“;
    Pokud e

    To se mi líbí

  11. koubel: Díky za doplnění. Mně osobně ale to, že je pak constructor enum nikdy nevadilo, protože iteruju jen přes pole pomocí normálního foru.

    Daniel: Asi záleží na konkrétní aplikaci – já třeba iteruju jen pomocí normálního foru přes pole. For-in považuju za něco jako reflexi v C#, takže se tomu snažím vyhnout. Tvůj kód opravdu selže – já jsem totiž předpokládal, že se nejdřív vytvoří prototypy a pak se použijí (tj. žádné míchání). Jak se píše v tom odkazovaném článku: „Altering prototypes during your script is a tricky and dangerous procedure.“
    S tím Function.prototype máš samozřejmě pravdu.
    Na Tvůj článek se těším 😉

    To se mi líbí

  12. Ještě k rozšiřování Object.prototype. Pokud budeš v Javascriptu opravdu iterovat pouze pole, nic se nestane. Pochybuji ovšem, že to zaručíš u všech skriptů, protože iterování objektů {}, je velmi časté prostě proto, že se pomocí {} implementují key-value based slovníky. A to naprosto všude a ve všech libraries.
    Typicky:
    $(‚#box‘).setStyles({width: ’20px‘, color: ‚red‘});
    Pokud rozšíříš Base objekt javascriptu (Object), ze kterého se vše dědí, nastavíš #boxu styl ‚extend‘ s hodnotou: function(arg) {… }.. což asi nechceš 😉

    To se mi líbí

  13. Kouknul sem na ten článek, z něj bych si radu fakt nebral. Věta “Altering prototypes during your script is a tricky and dangerous procedure.” je úplně mimo.
    String.prototype.nasrat = function() {}…
    Co to je jiného, než altering prototypes..?

    To se mi líbí

  14. Jistě, že je to altering prototypes. Je to myšleno tak (nebo aspoň já jsem si to tak vyložil a víceméně se s tím ztotožňuji), že skript by měl mít „deklarační“ a „výkonnou“ část, které by se neměly míchat (něco jako interface a implementation v Delphi).

    To se mi líbí

  15. To nevím proč by měl. Krása Javascriptu je právě v tom, že můžeš měnit již existující implementaci. Příklad:
    Sosnu si framework Mootools. Ten obsahuje třídu Element.
    Chci přidat nějakou funkcionalitu, ale nechci sahat do kódu frameworku.
    Proto ve svém kódu napíšu:
    Element.prototype.explode = function() {…}
    Pak stačí $(‚myBoss‘).explode();
    Je to jasné?

    To se mi líbí

  16. Tak Michale, rychle sem ten článek znovu prolétl. Snad se nebudeš zlobit, když tu udělám resumé pro náhodného návštěvníka.
    Na začátku musím říct, že si nezmínil, že některé techniky pod IE6/7/8 fungovat nebudou. Chápu proč, zmiňuješ SpiderMonkey, já jen že to někoho může zmást, když to přehlédne.

    Eval je nebezpečná technika, to jen tak naokraj bych zmínil. Eval is Evil, tak nějak patří k sobě 🙂

    Object initializers je pojem z C#3, v Javascriptu se říká object literal.

    JSON není object literal. Je to jeho subset. Nesmí obsahovat funkce, a klade vyšší nároky na syntax (pouze dvojité uvozovky, a to i u klíčů)

    Nikdy nedefinuj metody v konstruktoru (viz druhý díl seriálu)

    „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ě.“
    Není pravda, enumerabilní jsou všechny, avšak Internet Explorer má bug: http://jsfiddle.net/MPZwE/

    Opravdovou vícenásobnou dědičnost JS nemá (viz třetí díl seriálu)

    T2.prototype = new T1; je principiálně jediný správný postup, avšak máš pravdu v tom, že je špatný (nechceme volat konstruktor kvůli dědičnosti) Nicméně, lze to snadno obejít (viz druhý díl seriálu)

    „Následující řešení se mi zdá zajímavější:“ – ani tohle není ten správný způsob…

    „T2.prototype = T1.prototype;“ Tohle je moc 😉 Nelze předávat konstruktor tímto způsobem.. to bys pak měl dědičnost v obou směrech 😉 Cokoliv bys přidal do prototype T2, by měla i třída T1 🙂

    Všechno v Javascriptu není objekt. Javascript má totiž i primitivní typy. Ty sice můžeš boxovat, ale fakticky vytváříš nové objekty.

    Prototype není kontejner pro statické položky třídy. To je třída sama. (viz také můj článek, díl třetí)

    No a rozšiřovat Object prototype je také zlo… (zase článek 😉

    Prostě.. tady dlouho chyběla (a chybí) literatura. Proto je logické, že člověk dělá chyby. Já sem měl to štěstí (nebo tu smůlu;), že sem si všechny slepé cesty poctivě prošlapal 🙂

    http://zdrojak.root.cz/clanky/oop-v-javascriptu-i/

    To se mi líbí

  17. Platnost proměnných

    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);
    }
    }

    Ale mě to funguje

    var poleObjektu = [{name: "Jana"},{name:"Pavla"}];
    function lala(){
    for (var i = 0; i < poleObjektu.length; i++) {
    var item = poleObjektu[i];
    function la(){
    alert(item.name);
    }
    la();
    }
    }
    lala();

    Nebo to chapu blbě 😦

    To se mi líbí

  18. Ta tvoje ukázka kódu se liší tím, že sice vytvoříš v každé iteraci novou funkci, ale okamžitě ji zavoláš, nikam ji neukládáš. Takže se použije aktuální hodnota v dané iteraci.
    V mé ukázce se ta funkce někam uloží a až po dokončení cyklu se odněkud zavolá (po kliknutí třeba).

    To se mi líbí

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Google photo

Komentujete pomocí vašeho Google účtu. Odhlásit /  Změnit )

Twitter picture

Komentujete pomocí vašeho Twitter účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.