Třídy, dědičnost a OOP v Javascriptu – I

JavaScript

Jak funguje objektově orientované programování v Javascriptu? Má Javascript třídy nebo nemá? Jak se implementuje dědičnost? Na tyto otázky si odpovíme v sérii článků, a ukážeme si, že Javascript je flexibilní, objektově orientovaný jazyk, vhodný nejen pro rychlé prototypování, ale i pro vývoj složitých aplikací.

Seriál: OOP v Javascriptu (3 díly)

  1. Třídy, dědičnost a OOP v Javascriptu – I 15.3.2010
  2. Třídy, dědičnost a OOP v Javascriptu – II 22.3.2010
  3. Třídy, dědičnost a OOP v Javascriptu – III 29.3.2010

Začneme stručným shrnutím a sjednocením pojmů, kterým se v našem výkladu
nevyhneme, a poznámkami k textu.

Pokud vás nějaký aspekt jazyka zaskočí, nebo pokud se vám
bude zdát, že jsem něčemu nevěnoval dostatečnou pozornost, doporučuji odkaz: https://developer.mozilla.org/en/JavaScript, který by si měl uložit každý, kdo to s Javascriptem myslí vážně. 

Na všechny příklady budeme používat službu jsFiddle, díky které si můžete příklady rovnou naživo vyzkoušet.

Českou
terminologii použiji, pouze existuje-li ustálený český ekvivalent.
Nesouhlasím s nutností překladu do češtiny za každou cenu, ba co víc,
domnívám se, že je to činnost zbytečná a škodlivá. Bez znalosti
angličtiny se programátor stejně neobejde.

Ačkoliv budu psát
o dědičnosti a objektově orientovaném programování, zmíním i
funkcionální prvky jazyka. Martin Malý mi kdysi položil otázku: Je
nějaký důvod, proč by lidi v JavaScriptu měli programovat především „objektově“ a
ne „funkcionálně“?
Tato otázka klade falešné dilema. Nejlepší je znát a
využívat oba přístupy, a ty rozhodně nejsou zaměnitelné, spíše se
doplňují. Doufám, že na konci minisérie bude zřejmé jak. Ukážeme si
všechny obvyklé způsoby, jak vytvořit „třídu“, a to od nejjednoduššího po
nejsprávnější.

Tak má ten Javascript třídy, nebo nemá?

Jak by řekl sir
Humphrey, ano i ne. Javascript nemá klasické třídy. Avšak podle
definice „A class is a construct that is used as a blueprint (or
template) to create objects of that class.
“ (in: http://en.wikipedia.org/wiki/Class_(computer_science)) třídy
má. Mezi programátory Javascriptu pak platí konsensus, že za třídu
považujeme konstrukční funkci, krátce konstruktor, který využívá
vlastnosti prototype. Jak se taková konstrukční funkce s vlastností
prototype liší od klasické třídy, si povíme v průběhu článku.

Přehled pojmů

Scope a closure

Upozornění:
Podrobně se tématu věnoval Petr Staníček ve svém seriálu. Já jej zde
opakuji pro osvěžení a také proto, že další úhel pohledu nikdy není na
škodu.

Scope je rozsah viditelnosti proměnné. Scope
může být globální nebo lokální. Globální scope je jeden, a v prohlížeči
jej vždy reprezentuje objekt window. Lokální scope generuje v
Javascriptu pouze funkce. Scope si lze představit jako mercedes s
tmavými skly. Zevnitř je vidět ven, ale zvenčí není vidět dovnitř.
Javascript umožňuje do sebe funkce zanořovat. Tím se tvoří scope chain. Tady už příklad trochu kulhá, ale dejme tomu, že zaparkujeme mercedes v garáži, která má okna také ztmavená. Viz příklad Scope.

Closure je
pouze jiný název pro scope. Když hovoříme o closure nějaké funkce,
hovoříme o scope, ve kterém byla funkce deklarována. Proč je vnější
scope funkce tak důležitý, že si zasluhuje vlastní název? Je to proto,
že je na něj zevnitř funkce vidět, ať už funkci voláme z jakéhokoliv
místa v programu. To je velmi užitečné, protože v Javascriptu jsou
funkce prvotřídní objekty.
To zhruba znamená, že si je můžeme ukládat do proměnných nebo předávat
argumentem, čili zacházet s nimi jako s objekty. Funkce si vždy nese
odkaz na scope, ve kterém byla deklarována. Zanořování funkcí a to, že
každá funkce má vlastní closure, jsou funkcionální prvky jazyka
Javascript.Viz názorný příklad Closure.

Objekt

V Javascriptu platí, že vše co není primitivní typ je asociativní pole,
krátce objekt. Funkce je objekt, pole je objekt, i regulární výraz je
objekt. Pro začátečníky bývá matoucí, že vše je objekt, tedy
asociativní pole, a přesto existuje samostatný typ object. Jak je to
možné? Je to jednoduché: Object je v hierarchii všech typů nejvýše, je
tedy předkem pro všechny ostatní typy, díky čemuž všichni jeho potomci
dědí jeho vlastnosti. Objekt je zkrátka v Javascriptu vše, co umožňuje
přiřadit vlastnost, a předává se referencí.

http://jsfiddle.net/V7a7x/

Funkce, metoda, konstruktor, třída

Vše, co je zmíněno
v titulku, je v Javascriptu stále a jedna tatáž funkce. Proč jí tedy
nazýváme čtyřmi jmény? Protože pojmenování určuje roli, kterou funkce
hraje. V Javascriptu funkce slouží k více účelům.

Funkce, to je prostá definice. Není přiřazena k žádné třídě, k žádnému objektu, a podle konvence se píše v camelCase, tedy s malým písmenem na začátku.

var foo = function () {};

Pokud je funkce přiřazena nějakému objektu, říkáme jí Metoda. Rovněž ji píšeme v camelCase.

user.foo();

Konstruktor, neboli konstrukční funkce, je určena k vytváření instancí. Proto se jí také říká třída. Třída a konstruktor znamená v Javascriptu to samé. Píšeme ji v PascalCase, tedy s velkým písmenem na začátku, které nám naznačí, že bychom měli použít operátor new.

var Person = function() {};
var joe = new Person();

this, kontext

Klíčové slovo this odkazuje na kontext. Kontext je objekt, ve kterém funkci voláme. V následujícím příkladu vidíme, že voláme-li funkci bane přímo, je kontextem globální objekt window. Přiřadíme-li funkci objektu user, stane se kontextem user. Jak tečkový operátor přesně funguje, si povíme později.

var bane = function() {
    this.banned = true;
};
bane();

// true
alert(window.banned);

var user = {};
user.bane = bane;
user.bane();

// true
alert(user.banned);​

Příklad: http://jsfiddle.net/Nz9TJ/

Jak
je vidět, funkce můžeme volat nad různými objekty. Většinou hovoříme o
kontextu, ve kterém je funkce volána. Kontext můžeme funkci i vnutit,
pomocí klíčových slov call a apply, jak ukazuje následující příklad: http://jsfiddle.net/KnaWr/

Techniky vytváření tříd

Ukážeme
si dvě falešné a jednu správnou. Falešné, protože ve skutečnosti nejde
o vytváření instancí tříd, ale o konstrukci podobných objektů. Jaký je
v tom rozdíl, nám postupně osvětlí příklady.

Zneužití closure

// zajímavý, avšak špatný způsob vytváření "instancí" v Javascriptu
var Animal = function(p_name) {
    var name = p_name;
    return {
        showName: function() {
            alert(name);
        }
    }
};
var kitty = Animal('Kitty'); 
// alert 'Kitty'
kitty.showName();

// false
alert(kitty instanceof Animal);

Příklad: http://jsfiddle.net/4CXkY/

Popíšeme si, co vidíme. Funkce Animal přijímá parametr p_name, který si ukládá do lokální proměnné name. Následně vrací objekt s metodou showName, která vidí lokální proměnnou name
ve svém closure. Na dalším řádku vytvářím „instanci“ kitty. Všimněte
si, bez použití operátoru new. Ten je v tomto případě zcela zbytečný,
objekt, který funkce Animal vrací, si vytvářím sám. Na posledním řádku
vidíme využití „instance“ v praxi. Každá instance je unikátní, každá má
vlastní scope (v něm je uložena proměnná name). Proměnná name je
zapouzdřena, protože je lokální, nelze ji změnit odjinud, než z vnitřku
funkce Animal.

„Hurá!, to bylo jednoduché. Takhle
jednoduše, že se dělají třídy? V tom případě mám hotovo, ještě napsat
testy, podědit a… a sakra, co když budu mít privátní metodu, jak ji
otestuji? Nijak, no nic. Teď musím ještě vytvořit „třídu“ Cat, potomka
třídy Animal… hmm, jenže jak? Možná, že kdybych…“

Stop,
takhle ne! Výše uvedený příklad ilustruje pružnost Javascriptu, ale
rozhodně není správným způsobem, jak tvořit třídy. Ukázali jsme si jej proto, že naznačuje obvyklý způsob, jakým se v Javascriptu imitují
privátní proměnné, totiž pomocí lokálních proměnných schovaných v
closure.

„Privátní“ proměnné v Javascriptu – má to smysl?

Každý javascriptový programátor by si měl uvědomit, že snaha ultimátně zapouzdřit nějakou proměnou, či rovnou celou funkcionalitu, je většinou marná. Javascript je dynamický jazyk.
Pokud do své aplikace pustíme cizí kód, o kterém nevíme, co dělá, a
proto raději „zapouzdřujeme“, trpíme falešným pocitem bezpečí. Smiřme
se s faktem, že Javascript není vhodný jazyk pro psaní software na
ovládání jaderných elektráren
, a zkusme to brát jako jeho výhodu.

Pokud
v Javascriptu něco „zapouzdřujeme“, děláme to hlavně proto, abychom
čtenáři kódu naznačili: „Tohle je privátní, tak si toho nevšímej.
Nespoléhej, že tahle metoda bude vždy fungovat stejně, možná v příští
verzi nebude fungovat vůbec.
“ Mírně to naznačíme podtržítkem v názvu,
brutálně pomocí closure. Jestli někde má smysl simulovat privátní
proměnné pomocí closure, tak jedině u statických objektů, tedy modulů.

Moduly

Modul v Javascriptu rovná se statický objekt. Nelze vytvářet jeho instance. Implementace snad ani nemůže být jednodušší.

var console = {
    log: function(message) {
        // ... nějaký kód
    }
};

Douglas Crockford „vymyslel“ vlastní verzi modulu, která má, považte, privátní lokální proměnné.

var console = (function() {
    var iAmPrivate = 'foo';    
    return {
        log: function(message) {
            // ... nějaký kód
        }
    }
})();

Popišme
si kód: Vytváříme anonymní funkci, kterou okamžitě voláme
(ty kulaté závorky na konci). Vnitřní scope anonymní funkce je closure
metody log. Metoda log je v objektu, který vracíme, a který se ukládá
do proměnné console. Je nemožné z vnějšku změnit proměnnou iAmPrivate,
zato je velmi jednoduché přepsat objektu console metodu log. Proto je
hra na „privátní“ proměnné v Javascriptu převážně čas mařící manýrou. Přesto se tato technika občas používá, a to ze dvou rozumných důvodů:

  1. potřebujeme referenční proměnné, a nechceme špinit globální scope
  2. mikrooptimalizace

jQuery
je knihovna, která se maximálně vyhýbá znečištění globálního scope.
Fakticky má pouze dvě globálně viditelné proměnné, jQuery a $. Dolar
si však jako globální magickou über funkci vybralo více knihoven.
Proto jQuery navrhuje vlastní  kód zapouzdřit takto:

(function($) {
    /* some code that uses $ */ 
})(jQuery);

Začátečník
(a, co si budeme namlouvat, i profesionál), je rád, že namísto dlouhého j
Q u e r y, může všude psát sexy dolary, aniž by riskoval konflikt s
jinou knihovnou.

Druhým, a jen zřídka rozumným, důvodem jsou mikrooptimalizace (premature optimization is the root of all evil). 

(function() {
    var EventType = SomeNamespace.InnerNamespace.ClassName.EventType;
})();

Jak
lze vidět, vytváříme si lokální referenci na EventType nějaké třídy.
Kdykoliv budeme enumeraci EventType potřebovat, odkážeme se na lokální
proměnnou. Javascript tak nebude vyhodnocovat x tečkových operátorů
stále dokola. Toto má smysl, pokud chceme „zpřehlednit“ kód, a také,
pokud nám záleží na tom, aby funkce využívající EventType, byla zhruba
o tisícinu milisekundy rychlejší. Někdy to smysl má, protože Internet
Explorer
. Ale pouze pro výkonnostně kritické funkce, například $type ($type
je funkce pro detekci všech možných typů, se kterými se můžeme v
Javascriptu setkat). K modulům se ještě vrátíme, až budeme probírat
mixování.

„Vylepšené“ třídy

Následující příklad už vypadá lépe. Je tam operátor new (ten za nás vytváří objekt), používá se this (tím se na vytvořený objekt odkazujeme uvnitř metody), operátor instanceof funguje. Je to technika navržená Douglasem Crockfordem. Douglas si dokonce vytvořil vlastní názvosloví: metodě showName se říká privilegovaná, protože ačkoli je veřejná, má přístup k privátní lokální proměnné name.
Douglas Crockford udělal pro svět Javascriptu hodně, ale některé
články, věnované OOP a dědičnosti, se mu zrovna nepovedly. Ani tento
způsob není správný:

var Animal = function(p_name) {
    var name = p_name;
    // privilegovaná metoda
    this.showName = function() {
        alert(name);
    }
};
var kitty = new Animal('Kitty');
kitty.showName(); // 'Kitty' 
alert(kitty instanceof Animal); // true

(http://jsfiddle.net/EP7Mw/)

Předchozí
příklad „zneužití closure“ i tento mají společné, že ukazují „konečně
ten správný“ postup, jak mít v Javascriptu privátní členy. A oba jsou
špatné. Privátní členy můžeme akceptovat u modulů, ale tvořit třídy
tímto způsobem nelze. Vzdejte to. Funkcionální prvky Javascriptu mají
své využití jinde. Možná vám vrtá hlavou, proč jsou předchozí techniky
špatné. Každá nakonec selže na jednom z těchto bodů:

  • privátní lokální proměnné a metody neotestujete
  • closure a privilegované metody se pro každou instanci vytváří zas a znova, což je nemalá (a hlavně zbytečná) zátěž
  • nefunguje operátor instanceof
  • veškerá legrace skončí, až se pokusíte podědit takovou „třídu“
    • v potomku nelze volat metodu rodiče
    • již existující instanci nelze (elegantně) přidat nebo změnit vlastnost

Konec první části

Ukázali jsme si několik metod, jak v Javascriptu vytvářet objekty, představili jsme si jejich výhody a nevýhody, řekli si, kde se používají, a především – proč jsou špatné. V příští části se podíváme na „konečně správné“ řešení pomocí prototype.

Nepřehlédněte!

Autor článku Daniel Steigerwald vystoupí s přednáškou na téma Třídy, dědičnost a OOP v Javascriptu na letošní konferenci Internet Developer Forum 2010. Přijďte si jej (a samosebou i další přednášející) poslechnout a zeptat se jich na to, co vás zajímá, ve středu 7. dubna do Národní technické knihovny (registrace nutná).

Independent software gardener, libertarian, web applications consultant and trainer. Google Developer Expert since 2012.

Komentáře: 148

Přehled komentářů

gisat Odkdy funkce není objektem?
Daniel Steigerwald Re: Odkdy funkce není objektem?
peter Re: Odkdy funkce není objektem?
Daniel Steigerwald Re: Odkdy funkce není objektem?
olin Re: Odkdy funkce není objektem?
gisat Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Re: Odpověď na dotaz Daniela Steigerwalda
peter Re: Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Re: Odpověď na dotaz Daniela Steigerwalda
pr.rybar Re: Odpověď na dotaz Daniela Steigerwalda
gisat Re: Odpověď na dotaz Daniela Steigerwalda
Aichi Re: Odpověď na dotaz Daniela Steigerwalda
gisat Re: Odpověď na dotaz Daniela Steigerwalda
Aichi Re: Odpověď na dotaz Daniela Steigerwalda
gisat Re: Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Re: Odpověď na dotaz Daniela Steigerwalda
Michal Augustýn Re: Odpověď na dotaz Daniela Steigerwalda
Daniel Steigerwald Re: Odpověď na dotaz Daniela Steigerwalda
Farrell Re: Odpověď na dotaz Daniela Steigerwalda
jay Re: Odpověď na dotaz Daniela Steigerwalda
gisat Re: Odpověď na dotaz Daniela Steigerwalda
jay Re: Odpověď na dotaz Daniela Steigerwalda
MD Re: Odpověď na dotaz Daniela Steigerwalda
v6ak Re: Odpověď na dotaz Daniela Steigerwalda
pr.rybar Re: Odpověď na dotaz Daniela Steigerwalda
Daniel Steigerwald Re: Odpověď na dotaz Daniela Steigerwalda
MD Re: Odpověď na dotaz Daniela Steigerwalda
Daniel Steigerwald Re: Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Re: Odpověď na dotaz Daniela Steigerwalda
Michal Augustýn Re: Odpověď na dotaz Daniela Steigerwalda
MD Re: Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Re: Odpověď na dotaz Daniela Steigerwalda
Daniel Steigerwald Re: Odpověď na dotaz Daniela Steigerwalda
pk Re: Odpověď na dotaz Daniela Steigerwalda
Aleš Roubíček Pěkný článek + malá výtka k testům
Michal Augustýn Re: Pěkný článek + malá výtka k testům
Daniel Steigerwald Re: Pěkný článek + malá výtka k testům
Aleš Roubíček Re: Pěkný článek + malá výtka k testům
MD Re: Pěkný článek + malá výtka k testům
Aleš Roubíček Re: Pěkný článek + malá výtka k testům
Borek Bernard Re: Pěkný článek + malá výtka k testům
Michal Augustýn struktura + ladění anonymních funkcí + těšení
Tom5 Re: struktura + ladění anonymních funkcí + těšení
Martin Malý Re: struktura + ladění anonymních funkcí + těšení
Karel Re: struktura + ladění anonymních funkcí + těšení
Ondřej Žára Re: struktura + ladění anonymních funkcí + těšení
Daniel Steigerwald Re: struktura + ladění anonymních funkcí + těšení
Ondřej Žára Re: struktura + ladění anonymních funkcí + těšení
Daniel Steigerwald Re: struktura + ladění anonymních funkcí + těšení
Michal Augustýn Re: struktura + ladění anonymních funkcí + těšení
Aleš Roubíček Re: struktura + ladění anonymních funkcí + těšení
mizuki Re: struktura + ladění anonymních funkcí + těšení
MD funkce objekt/neobjekt?
Ondřej Žára Re: funkce objekt/neobjekt?
Daniel Steigerwald Re: funkce objekt/neobjekt?
Mazarik clanok
Daniel Steigerwald Re: clanok
fos4 povedený článek
Daniel Steigerwald Re: povedený článek
bzuK Thumbs up
logik Pochvala
danaketh Moc pěkné
aprilchild vyborne
Martin Staněk pohled na článek
aprilchild Re: pohled na článek
Martin Staněk Re: pohled na článek
Aleš Roubíček Re: pohled na článek
karmi Re: pohled na článek
Daniel Steigerwald Re: pohled na článek
Martin Staněk Re: pohled na článek
Martin Malý Re: pohled na článek
s Oh!!
Michal Augustýn Re: Oh!!
pr.rybar Re: Oh!!
s Re: Oh!!
pr.rybar Re: Oh!!
s Re: Oh!!
pr.rybar Re: Oh!!
Rene Článek
v6ak Proč privátně?
Daniel Steigerwald Re: Proč privátně?
v6ak Re: Proč privátně?
Daniel Steigerwald Re: Proč privátně?
v6ak Re: Proč privátně?
Michal Augustýn Re: Proč privátně?
Daniel Steigerwald Re: Proč privátně?
Daniel Steigerwald Re: Proč privátně?
Michal Augustýn Re: Proč privátně?
junix Re: Proč privátně?
Aichi Re: Proč privátně?
David Struktura článku
Daniel Steigerwald Re: Struktura článku
David Re: Struktura článku
Bauglir Špatný článek
David Grudl Re: Špatný článek
pr.rybar Re: Špatný článek
koroptev Re: Špatný článek
peter Re: Špatný článek
Daniel Steigerwald Re: Špatný článek
xx Re: Špatný článek
pr.rybar Re: Špatný článek
Jakub Nešetřil Re: Špatný článek
pr.rybar Re: Špatný článek
Bauglir Re: Špatný článek
xx Re: Špatný článek
Aleš Roubíček Re: Špatný článek
xx Re: Špatný článek
koroptev Re: Špatný článek
Bauglir Re: Špatný článek
Daniel Steigerwald Re: Špatný článek
olin Re: Špatný článek
MD jiny jazyk
Ondřej Žára Re: jiny jazyk
pr.rybar Re: jiny jazyk
koroptev Re: jiny jazyk
koroptev se pridam ke kritice
David Grudl Re: se pridam ke kritice
koroptev Re: se pridam ke kritice
olin Re: se pridam ke kritice
pr.rybar Re: se pridam ke kritice
koroptev Re: se pridam ke kritice
peter Re: se pridam ke kritice
Daniel Steigerwald Re: se pridam ke kritice
peter Re: se pridam ke kritice
Michal Augustýn Re: se pridam ke kritice
peter Re: se pridam ke kritice
Daniel Steigerwald Re: se pridam ke kritice
paranoiq Re: se pridam ke kritice
belzebub Re: se pridam ke kritice
Michal Augustýn Re: se pridam ke kritice
Daniel Steigerwald Re: se pridam ke kritice
junix OOP, dedicnost
Daniel Steigerwald Re: OOP, dedicnost
junix Re: OOP, dedicnost
v6ak Re: OOP, dedicnost
Daniel Steigerwald Re: OOP, dedicnost
junix Re: OOP, dedicnost
aprilchild Re: OOP, dedicnost
Timy Re: OOP, dedicnost
aprilchild Re: OOP, dedicnost
Daniel Steigerwald Re: OOP, dedicnost
Daniel Steigerwald Re: OOP, dedicnost
aprilchild Re: OOP, dedicnost
v6ak Re: OOP, dedicnost
aprilchild Re: OOP, dedicnost
v6ak Re: OOP, dedicnost
hon2a Dekuji za velmi kvalitni clanek
brouk nechápu tvrzení, že #nefunguje operátor instanceof
Zdroj: http://www.zdrojak.cz/?p=3191