Třídy, dědičnost a OOP v Javascriptu - I
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
- Třídy, dědičnost a OOP v Javascriptu - I
- Třídy, dědičnost a OOP v Javascriptu - II
- Třídy, dědičnost a OOP v Javascriptu - III
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í.
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ů:
- potřebujeme referenční proměnné, a nechceme špinit globální scope
- 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
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á).
Školení Google+ pro firmy

- Jak využít Google+ pro firemní komunikaci a marketing.
- Čím se liší Google+ od Twitteru a Facebooku z pohledu firemního využití.
- Jak využít Google+ v souladu s pravidly užívání.
- Založení Google+ Page (Stránky) krok po kroku, včetně praktických tipů.
Detailní informace o školení Google+ »
Seriál OOP v Javascriptu
- Třídy, dědičnost a OOP v Javascriptu - I
- Třídy, dědičnost a OOP v Javascriptu - II
- Třídy, dědičnost a OOP v Javascriptu - III
Přehled názorů
Tento text je již více než dva měsíce starý. Chcete-li na něj reagovat v diskusi, pravděpodobně vám již nikdo neodpoví. Pro řešení aktuálních problémů doporučujeme využít naše diskusní fórum.