V předchozím díle jsme probrali základní pojmy OOP a dva špatné způsoby deklarace tříd: zneužití closure a Crockfordovy pokusy o privátní proměnné. Správný způsob si ukážeme nyní.
Jak už bylo řečeno, funkce v Javascriptu hraje dvě hlavní role. Buď je funkcí, nebo je konstrukční funkcí, krátce třídou. Klíčem k jedinému správnému vytváření tříd je vlastnost jménem prototype. Má ji každá funkce, a její výchozí hodnota je prázdný objekt.
var fn = function() {}; alert(typeof fn.prototype == 'object'); // true
Příklad: http://jsfiddle.net/pNQr5/
Jak ukazuje následující příklad, prototype se použije vždy, když konstruktor zavoláme operátorem new. Určuje, jaké vlastnosti má vytvořená instance mít.
var Animal = function(name) { this.name = name; }; Animal.prototype = { getName: function() { return this.name; } }; var mici = new Animal('Mici'); var kevi = new Animal('Kevi'); alert(mici.getName()); // Mici alert(kevi.getName()); // Kevi
Příklad: http://jsfiddle.net/ZT4AW/
Takto vypadá přirozená deklarace tříd, tak jak plyne z návrhu jazyka. Všimněte si, že nevytváříme žádné privilegované metody, ani neukládáme stav do closure. Kdybychom to udělali, zavřela by se nám cesta k pozdějším modifikacím i dědičnosti.
Jak funguje operátor new?
- vytvoří instanci podle prototype
- zavolá konstruktor v jejím kontextu (uvnitř funkce se na instanci odkazujeme pomocí this)
- vrátí vytvořenou instanci
Jediná správná technika vytváření instancí je spojení operátoru new a vlastnosti prototype. Proč? Zapamatujme si, že instance vytvořená operátorem new je „odrazem v zrcadle“ objektu prototype. Kdykoliv změníme prototype, přidáme metodu nebo vlastnost, změna se projeví ve všech instancích, a to i v těch již vytvořených. Následující příklad to dokazuje:
var Animal = function(name) { this.name = name; }; Animal.prototype = { getName: function() { return this.name; } }; var mici = new Animal('Mici'); var kevi = new Animal('Kevi'); alert(mici.getName()); // Mici alert(kevi.getName()); // Kevi // chci přidat syntax sugar metodu Animal.prototype.alertName = function() { alert(this.getName()); }; // hle!, metoda se přidala k již existující instanci mici.alertName(); // chci změnit getName Animal.prototype.getName = function() { return 'Mé jméno je: ' + this.name; }; // ha!, metoda se změnila všem existujícím instancím kevi.alertName(); // důkaz, že metody jsou sdílené alert(mici.getName === kevi.getName); // true alert(mici.getName === Animal.prototype.getName); // true
Vlastnost prototype je všemi instancemi jedné třídy sdílená. Jak tedy nastavujeme vlastnosti konkrétní instanci? Jednoduše pomocí tečkového operátoru. Můžeme si to představit jako kreslení rtěnkou na zrcadlo.

Jak ukazuje následující příklad, tečkový operátor preferuje instanční vlastnosti. Když v předposledním řádku vlastnost name instanci smažeme, vrací se hodnota z prototype.
var Person = function(name) { this.name = name; }; Person.prototype.name = 'noname'; var joe = new Person('Joe'); alert(joe.name == 'Joe'); // true delete joe.name; alert(joe.name == 'noname'); // true
Příklad: http://jsfiddle.net/5ALHm/
To, že je prototype sdílen všemi instancemi, svádí některé programátory k tomu, že považují prototype za „kontejner pro statické položky třídy (constructor function)“, což je omyl. Prototype je vzor pro tvorbu instancí. Statické položky se správně přiřazují pouze ke konstruktoru.
// statické vlastnosti přiřazujeme pouze ke konstruktoru Person.I_AM_CONSTANT = 42; Person.staticLookupMethod = function() {}; Person.staticArray = [];
Z čeho tento omyl pramení, je zřejmé: Každého programátora znalého klasických tříd dříve nebo později napadne napsat jasně instanční vlastnost přímo do prototype.
// takhle ne, instanční objekty do prototype nepatří var Person = function(skill) { this.skills.push(skill); }; Person.prototype.skills = []; var creativePerson = new Person('creativity'); var stupidPerson = new Person('stupidity'); // špatně, vypíše se 'creativity,stupidity' alert(stupidPerson.skills); // správně, instanční objekty až v konstruktoru var Person2 = function(skill) { this.skills = []; this.skills.push(skill); }; var creativePerson2 = new Person2('creativity'); var stupidPerson2 = new Person2('stupidity'); // správně, skills má být pouze stupidity alert(stupidPerson2.skills);
Porozumět tomuto příkladu je klíčové, pokud chceme pochopit hlavní rozdíl mezi klasickou třídou a třídou, jak ji chápe Javascript. Operátor new vytváří mělkou kopii objektu prototype. Pole skills,
definované v prototype, se tak stane de facto statickou vlastností, ale je zavádějící ji tak nazývat. Je zřejmé, že ke statickým vlastnostem nepřistupujeme přes instance. Shrňme si tedy, co kam patří:
- statické vlastnosti a metody přiřazujeme konstruktoru
- instanční metody definujeme v prototype
- instanční objekty vytváříme v konstruktoru (nebo ostatních metodách)
Přesto se názvy instančních objektů do prototype občas zapisují, avšak neinicializované, a pouze kvůli dokumentaci.
// ideálně var Person = function(skill) { this.skills = []; this.skills.push(skill); }; /** * Person skills in array of strings * @type {array} */ Person.prototype.skills = null; // pozor, inicializovat až v konstruktoru var creativePerson = new Person('creativity'); var stupidPerson = new Person('stupidity'); // správně, skills má být pouze stupidity alert(stupidPerson.skills);
Objektová dědičnost
Malá odbočka: V Javascriptu se často hovoří o objektové dědičnosti. Následující technika, myslím, pěkně ilustruje, co to znamená, když objekt dědí z objektu. V praxi ji nemá smysl používat, ale bude se nám hodit při výkladu implementace dědičnosti.
// funkce beget vytvoří poděděný objekt var beget = function(parent) { // F je pomocný dočasný konstruktor var F = function() {}; F.prototype = parent; var child = new F; return child; }; var parent = {}; // parent je objekt var child = beget(parent); // a child také // předkovi nastavím nějakou hodnotu parent.property = 'value'; // a vida, potomek ji má též (opačně to samozřejmě nefunguje) alert(child.property); // 'value'
Příklad: http://jsfiddle.net/SgXxK/
Možnost měnit instance i poté, co byly vytvořeny, je silným dynamickým prvkem jazyka Javascript. Dejme tomu, že používáme externí knihovnu a chceme opravit bug v jedné metodě. Můžeme samozřejmě přímo opravit kód. Co když ale chceme opravu zveřejnit ve fóru, ba co hůř, distribuovat v samostatném souboru jako dočasný fix? Pokud je metoda definovaná přes prototype, je oprava snadná.
SomeLibrary.SomeClass.prototype.buggyMethod = function() { // nový kód };
Co by se však stalo, kdyby konstruktor třídy SomeClass vypadal takto?
var SomeClass = function() { // takhle metody nikdy nedefinovat! this.buggyMethod = function() {}; };
Každá instance SomeClass by přepsala buggyMethod ze SomeClass.prototype. Oprava z vnějšku by byla nemožná. Museli bychom zasáhnout přímo do konstruktoru. Pokud bychom v konstruktoru definovali takto všechny metody (a takových příkladů je internet plný), uzavřeli bychom si cestu k pozdějším změnám. A hlavně bychom vytvářeli spoustu metod zbytečně, zas a znova, pro každou instanci zvlášť.
Daniel Steigerwald nabízí školení a konzultace JavaScriptu. Bližší informace zájemci naleznou na daniel.steigerwald.cz
Na co si dát u prototype pozor
Prototype je mocný nástroj, a jako každý takový, může napáchat dost škody. Ukážeme si dva příklady.
Modifikace Object.prototype je zlo
Object je v hierarchii všech typů nejvýše, ale object se používá také jako datový typ, asociativní pole, kde klíčem je řetězec (string) a hodnotou cokoliv. Zde vidíme dva způsoby, jak objekt vytvořit:
var obj1 = {}; var obj2 = new Object(); alert(obj1.constructor === obj2.constructor); // true
Příklad: http://jsfiddle.net/QDwdW/
Co se stane, když se rozhodneme, že úplně každý objekt by měl mít nějakou tu šikovnou metodu, třeba alert?
// tohle nikdy nedělejte Object.prototype.alert = function() { alert(this); }; (5).alert(); "Modifikace Object.prototype je zlo".alert();
Příklad: http://jsfiddle.net/kUTMv/
Jak vidíme, možnost přidat metodu všem objektům je fantastická. Má pouze dvě malé chyby. Za prvé tím rozbijeme všechny enumerace, a za druhé: rozbijeme tím všechny enumerace. Teoreticky jde o jednu a tu samou chybu, nicméně chyba je natolik závažná, že jsem ji považoval za nutné zmínit dvakrát.
// tohle nikdy nedělejte Object.prototype.alert = function() { alert(this); }; var styles = { color: 'red', height: 20 }; // vypíše true alert(styles.alert != null) for(var style in styles) { alert(style + ' ' + styles[style]); }
Příklad: http://jsfiddle.net/sA4Rs/
Cyklus for in při enumeraci prochází i klíče, které byly přidány do Object.prototype, takže pokud styles přiřadíme nějakému elementu, nastavíme mu krom barvy a šířky i alert, což asi nechceme. Že modifikovat Object.prototype je zlo, bylo napsáno na mnoha a mnoha místech. Bohužel, na mnoha a mnoha jiných místech, i v jinak dobrých článcích, tomu bylo naopak.
Kdy ještě může ještě být modifikace prototype nebezpečná?
Předchozí příklad naznačil, že pomocí prototype můžeme rozšiřovat existující třídy. Co kdybychom třeba poli přidali metodu each?
Array.prototype.each = function(fn, context) { for(var i = 0, l = this.length; i < l; i++) { fn.call(context || this, this[i], i, this); } }; ['a', 'b', 'c'].each(function(item) { alert(item); });
Příklad: http://jsfiddle.net/SdBhb/
Tento způsob je bezpečný pouze pokud zaručíme, že v aplikaci bude vždy jedině náš vlastní kód. Mohlo by se totiž stát, že i někoho jiného napadne přidat poli metodu each. V tom případě by byla naše vlastní implementace přepsána, případně my bychom přepsali each někomu jinému. Prohlížeč jako platforma pro vývoj webových aplikací je typicky heterogenní prostředí. Lidsky řečeno: widget napsaný nad knihovnou Mootools a jiný, napsaný pomocí knihovny PrototypeJS, spolu v jedné stránce fungovat nebudou. Existuje řada Javascriptových knihoven, ale jen tyto dvě (z těch, co stojí za řeč) modifikují prototype nativních typů.
Nativní typy
Použil jsem nový výraz, co znamená? Nativní typy jsou ty, které jsou v prohlížeči vestavěné. Vyjmenujme si ty, které se nachází ve všech prohlížečích: Array, Boolean,
. Typů je mnohem více, většinu z nich však podporuje až Internet Explorer 8. Názvy nejsou nic jiného než reference na konstruktory.
Date, Error, Function, Number, Object, RegExp, String
alert([].constructor === Array); // true
Příklad: http://jsfiddle.net/DjFus/
Pamatujme si, že jejich prototype bychom neměli modifikovat, pokud si nechceme zadělat na konflikty. Naopak zcela bezpečně lze modifikovat prototype tříd vlastních nebo tam, kde víme, že jiný než náš Javascript nepoběží (zpravidla Javascript na serveru).
Implementace dědičnosti
Konečně se dostáváme k hlavnímu tématu, implementaci dědičnosti. Jak řekneme Javascriptu, aby se třída Employee stala potomkem třídy Person? Jelikož nejsme ve Star Treku, nijak. Musíme mu to napsat. A předtím to někde vyčíst. V mnoha učebnicích i článcích je doporučován tento, sice funkční, ale jinak špatný, způsob:
// učebnicový nepraktický příklad var Parent = function() {}; var Child = function() {}; Child.prototype = new Parent(); // voláme konstruktor, to nechceme
Důležitý je poslední řádek. Vzpomeňme, co jsme si řekli o operátoru new, totiž že každá změna prototype se ihned projeví na všech instancích. Přiřadíme-li tedy instanci Parent do prototype Child, máme prototypovou dědičnost.
Proč jsem označil příklad jako nepraktický? Jak vidíme, při každém dědění se volá konstruktor Parent. To rozhodně nechceme, protože ten může třeba alokovat zdroje. Existuje lepší způsob – ale nejprve si obě třídy ukažme:
// deklarace třídy Person var Person = function(name) { this.name = name; }; Person.prototype.getName = function() { return this.name; }; // deklarace třídy Employee var Employee = function(name, salary) { // tady bych rád zavolal konstruktor Person, a předal mu name this.salary = salary; }; Employee.prototype.getSalary = function() { return this.salary; }; Employee.prototype.getName = function() { // tady bych rád zavolal přepsanou metodu Person getName };
Jak se třída Employee stane potomkem třídy Person? Pamatujete co jsme si říkali o dědění objektů? Nic nám nebrání podědit prototype, třeba pomocí takovéto funkce:
var extends = function(child, parent) { // F je pomocný dočasný konstruktor var F = function() { }; F.prototype = parent.prototype; child.prototype = new F(); }; // příklad volání extends(Employee, Person);
Kompletní příklad
Nyní si ukážeme kompletní příklad Javascriptové dědičnosti, včetně volání přepsané metody. Mimochodem, použitá technika je navlas stejná jako ta, kterou Google používá ve své vlastní, nedávno zveřejněné, Javascriptové knihovně Google Closure.
// pomocná funkce pro dědění var extends = function(child, parent) { // F je pomocný dočasný konstruktor var F = function() {}; F.prototype = parent.prototype; child.prototype = new F(); // konvence pro volání přepsaných metod child._superClass = parent.prototype; // dobrým zvykem je, aby instance odkazovala na svůj konstruktor child.prototype.constructor = child; }; // třída Person var Person = function(name) { this.name = name; }; Person.prototype.getName = function() { return this.name; }; // třída Employee var Employee = function(name, salary) { // zavoláme bázový konstruktor Person.call(this, name); this.salary = salary; }; // podědíme extends(Employee, Person); Employee.prototype.getSalary = function() { return this.salary; }; // přepisujeme metodu z třídy Person Employee.prototype.getName = function() { // voláme přepsanou metodu var name = Employee._superClass.getName.call(this); return name + ' (zaměstnanec)'; }; // vytvoříme instanci var joe = new Employee('Joe', 1000); // zkusmo přidáme metodu bázové třídě Person Person.prototype.setName = function(name) { this.name = name; }; // a teď tuto metodu vyzkoušíme na instanci Employee joe.setName('Pepa'); // všechny tyto testy musí projít alert([ joe.getName() == 'Pepa (zaměstnanec)', joe.getSalary() == 1000, joe instanceof Person, joe instanceof Employee, typeof Employee == 'function', typeof joe == 'object', joe.constructor == Employee, Employee._superClass == Person.prototype ]);
Příklad: http://jsfiddle.net/Eu8U8/
Takto se v Javascriptu správně implementuje dědičnost. Za zmínku stojí pomocná funkce extends. Javascriptu by sice pro vyjádření dědičnosti slušelo klíčové slovo, ale jak uvidíme později, obejdeme se pohodlně i bez něj. Funkce extends zakládá řetěz prototypové dědičnosti (prototype chain).
Volání přepsaných metod a konstruktorů
V konstruktoru Employee vidíme volání konstruktoru Person. V metodě getName pak volání přepsané metody. Je dobré si uvědomit, že volání přepsané metody:
var name = Employee._superClass.getName.call(this);
je pouze konvence. Klidně bychom mohli napsat:
var name = Person.prototype.getName.call(this);
… nebo bychom mohli zavolat i úplně jinou metodu:
var name = Object.prototype.toString.call(this);
Všimněme si volání metody call. Tím doslova říkáme: zavolej tuto metodu nad touto instancí. Tady bychom, stejně jako inženýři z Googlu, mohli skončit – a také pro dnešek skončíme. Téma by však nebylo kompletní, kdybychom si neřekli, jak lze zápis třídy i dědičnosti zjednodušit, jaké jsou další objektové techniky (agregace, mixování) a jak se k problému staví ostatní Javascriptové knihovny. Právě to bude námětem poslední části.
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á).
Přehled komentářů