Přejít k navigační liště

Zdroják » JavaScript » Třídy, dědičnost a OOP v Javascriptu – II

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

V předchozím článku jsme si ukázali, jak se v Javascriptu řeší zapouzdření a objekty, ukázali si nejčastěji používané postupy a vysvětlili si, proč jsou špatné. V dnešním pokračování si ukážeme, jak se dědičnost v Javascriptu implementuje správně, pomocí prototypů.

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?

  1. vytvoří instanci podle prototype
  2. zavolá konstruktor  v jejím kontextu (uvnitř funkce se na instanci odkazujeme pomocí this)
  3. 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

http://jsfiddle­.net/naqph/

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.

rtěnka 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.pro­totype. 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.steiger­wald.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,
Date, Error, Function, Number, Object, RegExp, String
. 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.

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 Ja­vascript 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._super­Class.getName­.call(this);

je pouze konvence. Klidně bychom mohli napsat:

var name = Person.prototy­pe.getName.ca­ll(this);

… nebo bychom mohli zavolat i úplně jinou metodu:

var name = Object.prototy­pe.toString.ca­ll(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á).

Komentáře

Subscribe
Upozornit na
guest
86 Komentářů
Nejstarší
Nejnovější Most Voted
Inline Feedbacks
View all comments
jarda

Díky, super.

Ondřej Žára

Dobré ráno,

pěkný článek, dobrá práce. Kdyby byl článek komentářem, klikl bych na plus :)

  1. Možná by nebylo na škodu detailněji osvětlit povahu vazby mezi instancí a prototypovým objektem konstrukční funkce, už proto, že řada implementací dovoluje tuto vazbu ex post měnit. Formulace „Operátor new vytváří mělkou kopii objektu prototype“ mi v tomto směru přijde nedostačující.
  2. Byl bych pro uvedení původu citace, použité u rozšiřování Object.prototype. Případný neznalý čtenář si tak může rozšířit obzory a zároveň citaci náležitě ocenit :)
  3. Jak se, Danieli, stavíte k nutnosti uvádět v každém volání metody předka (přepsané metody) název třídy potomka? Mám zde na mysli kód
    Potomek.prototype.neco = function() {
      return 1 + Potomek._superClass.neco.call(this);
    }

    Je zřejmé, že kvůli možnosti rekurzivního volání nelze zápis suplovat pomocí this.constructor._superClass.neco, ale přijde mi implementačně nepříjemné v každém takovém volání explicitně zmiňovat třídu (budoucí komplikace při přejmenování, refactoringu, …).

kvr

Článek taky chválím, příliš nového jsem se sice nedozvěděl, ale pro spoustu lidí neznalých problematiky bude jistě užitečný. Hodnotit se dá i článek :)

K tomu uvádění názvu třídy při volání – pro pochopení principu je to explicitní uvedení dobré, v rámci vývoje si pak lze třeba superc v rámci closure definovat ručně jako superc = Person.prototype, nebo uvádím příklad z praxe pro automatizované řešení:


dr.registerClass("dr::gui::PopupInfo", "dr::gui::GuiObject", function(event, options)
{
this.z_index = dr.gui.GuiUtil.advZIndex();
superc._construct.apply(this, [ null, dr.createElement("div", {
$class: "dr-gui-PopupInfo-main",
$style: "position: absolute; z-index: "+this.z_index+";",
}), {} ]);
...
return this;
},
{
_$require: [ "dr::gui::GuiUtil" ],
_$css: [ "dr/gui/PopupInfo.css" ],
close: function()
{
...
superc.close.apply(this, arguments);
},
});

Definici superc (či dalších proměnných) nepíše vývojář ručně, ale v rámci closure přidává proces na serveru. Na klienta tak už jde značně modifikovaný kompletní a korektní javascript.

Některé javascript frameworky umožňují i more-human-friendly zápisy jako $super() (tzn. v závistlo na kontextu funkce), ale za cenu složitě definovaných proxy, které celý běh zpomalují a kromě relativně hezčího (ale často nedostačujícího) zápisu vlastně nic nepřináší.

<div class=„opin-edited-info“>22. 3. 2010 13:48 redakčně upravil Martin Malý, důvod: Úprava formátování kódu</div>

22. 3. 2010 13:50 redakčně upravil Martin Malý, důvod: Úprava formátování kódu
kvr

Hm, tak jsem čekal, že code bude mít i pre, takže ještě jednou:

dr.registerClass("dr::gui::PopupInfo", "dr::gui::GuiObject",
    function(event, options)
    {
        this.z_index = dr.gui.GuiUtil.advZIndex();
        superc._construct.apply(this, [ null, dr.createElement("div", {
            $class:     "dr-gui-PopupInfo-main",
            $style:     "position: absolute; z-index: "+this.z_index+";",
        }), {} ]);
        ...
        return this;
    },
{
    _$require:          [ "dr::gui::GuiUtil" ],
    _$css:              [ "dr/gui/PopupInfo.css" ],
    close:              function()
    {
        ...
        superc.close.apply(this, arguments);
    },
});

Ondřej Žára
  1. Ne, tušíte špatně, mířím jinam. Mám zde na mysli skrytou vlastnost [[Prototype]], v implementacích nazývanou __proto__ (viz ECMA-262, sekce 13.2.2).

    Právě tato vlastnost, používaná při přístupu k vlastnostem objektu, je klíčová pro implementaci prototypové dědičnosti (při volání funkce s operátorem new – a jen v tomto případě – je hodnota [[Prototype]] nastavena na mělkou kopii prototypového objektu volané funkce). Proto je v řadě implementací možné (i když jde, i s ohledem na pojmenování, o nestandardní fíčuru) napsat

    var A = {ahoj: "jardo"};
    var B = {};
    B.__proto__ = A;
    alert(B.ahoj); // "jardo"

    Osobně si myslím, že pochopení této vlastnosti/vazby je důležitým krokem při poznávání krás prototypové dědičnosti :)

Ondřej Žára

Nejde ani tak o to, jestli nám naše implementace dovoluje __proto__ měnit, jako spíš o skutečnost, že nějaká obdobná vlastnost (i když třeba není čitelná) musí v každém interpretu existovat (neb to vyžaduje specifikace).

Nakonec je úplně jedno, jestli k [[Prototype]] smíme přistupovat nebo ji dokonce měnit. Klíčová informace je, že tato skrytá vlastnost je u všech objektů vytvořených operátorem new (to je ta hlavní odlišnost od ostatních způsobů tvorby objektů), že odkazuje na prototypový objekt funkce konstruktoru a že je použita jako (rekurzivní) fallback při veškerých přístupech k vlastnostem, které nejsou definovány přímo natvrdo v našem objektu.

karf

Výborný seriál, konečně rozumný přístup k prototypové dědičnosti. Nemám rád, když si každá knihovna vytváří vlastní neprůhledný systém tříd. Jen opravdu malinké rýpnutí:

Píšete, že v JS je konvencí nazývat třídou konstrukční funkci. Potom se toto:

child._superClass = parent.prototype;

jeví jako trochu zavádějící pojmenování, ne?

EskiMag

Výborný článok (aj celý seriál), presne v tejto oblasti som si chcel/potreboval prehĺbiť znalosti. Rád by som len upozornil na drobnú chybičku (ak tomu správne rozumiem): v časti Modifikace Object.prototype je zlo, v prvej ukážke na 3. riadku by asi malo byť alert(obj1.constructor === obj2.constructor); // true (jestli se nepletu :-D)

fanoush

Pekny a hodnotny clanek, diky. Jenom ty prirovnani mi tam pripadaji spis rusive. Nejak mi tam nepasujou a v obou pripadech mi vubec nepomohly k pochopeni veci. Nad mercedesem minule jsem chvili premyslel ale dneska jsem rtenku uz radsi rychle preskocil a cetl dal. Pak jsme se k ni vratil ale stejne nevim co tim autor myslel. Zbytecny balast u jinak dobreho clanku.

Aleš Roubíček

Že na zrcadle je vidět, to co se v něm odráží + to co je na něm nakreslené rtěnkou. A zároveň to, co je nakreslené rtěnkou není projektováno zpět na původní obraz. Já myslím, že je to pěkné přirovnání…

Karel

Tak teď jsem to konečně pochopil :-) Díky.

fanoush

aha, nojo, to pak sedi. Akorat jsem myslel puvodne ze je tam to prirovnani jako napoveda a ono je to tam spis jako tajenka kterou clovek vylusti (nebo nevylusti) az popisovanou vec pochopi bez napovedy :-)

Bauglir

Tady bych dodal, že obecně je rozšiřování vestavěných typů zlo, ale je výjimka.
V prosinci 09 byl schválen ES5, který přidává nové metody vestavěným objektům, například indexOf u pole. V tomto případě je již jasně dané, že tato metoda je součástí jazyka a má jasně danou syntaxi i funkcionalitu, takže bych se nebránil rozšíření prototypu Array.prototype o tuto funkci pro prohlížeče, kde neexistuje

​if (Array.prototype.indexOf === window.undefined)
{
Array.prototype.indexOf = function (index, fromPos)
{
//tady je kód dané funkce
}
}

Jinak pokud pomineme neshody v názvosloví (třída :) ), tak rozhodně tentokrát palec nahoru :)

Bauglir

​if (Array.prototype.indexOf === window.undefined)
{
  Array.prototype.indexOf = function (index, fromPos)
  {
    //tady je kód dané funkce
  }
}

v6ak

No ale obecně pro <b>správnou a úplnou</b> implementaci metod, které v některých prohlížečích nefungují, bych považoval za skoro bezproblémové. Problém může nastat snad jen tehdy, když někdo z existence této metody bude usuzovat na novější implementaci a z toho na přítomnost jiných metod. Ale tady bych spíš viděl chybu v úsudku o verzi implementace.

BTW: Škoda, že nefunguje toto: http://ideone.com/3LTlt7yY

Karel

Jak moc mi první díl přišel zbytečný a špatný, tak moc se mi tenhle druhý díl líbí. Teď už je asi pozdě ten první díl úplně smazat a začít tímhle dílem, že?

Příště se, prosím, k „věci“ dostaňte už v prvním díle a nezačínejte tím, jak se to dělat nemá. Z tohoto (bohužel až druhého) dílu je vidět, že věci rozumíte a i to umíte krásně podat. Tento druhý díl skutečně dělá celému serveru velkou čest.

MD

+1

cavo

28. // ha!, metoda se změnila všem existujícím instancím
29. kevi.aler­tName();

na r. 29 asi ma byt kevi.getName(); nie?

Matěj Konečný

Moc pěkný článek. Jen co se vysvětlování (doufám, že to tu už někdo neříkal), jak je provázaná vlastnost prototype s instancemi, popřípadě že Child.prototype=new Parent() je špatně, týče, myslím si, že dobře zpracovaný obrázek graficky znázorňující provázanost instanční vlastnosti __proto__ atd. osvětlí problém mnohem lépe.

Perfektně využil obrázky Nicholas Z. Zakas ve své knize Professional JavaScript for Web Developers. (http://jdem.cz/eajk5)

Matěj Konečný

To, že __proto__ je nestandartní a IE ji nepodporuje je pro účely vysvětlení jedno.

Michal Augustýn

Ten trik s vytvořením pomocného objektu a správné navázání prototype do chainu se mi líbí a neznal jsem ho (mluvím o funkci extends). První nová věc, kterou jsem se dozvěděl, ale docela zásadní ;-) Těším se na další díl! :)

junix

Musim rict, ze jak jsem se na tento dil tesil, tak me pomerne zklamal. Kdyz jsem prvnimu dilu vytykal, ze by se mel zdrzet oznacovani nekterych pristupu za spatne, tak tentokrat bych uz vubec neuvadel popsany princip za „spravny“. Spis bych ho oznacil za jednu z cest, kterou voli nekolik frameworku, ale rozhodne ne za cistou. Mozna v nekterych ohledech zjednodusujici.

Drive nez se zacne popisovat tento zpusob, melo by byt dobre vysvetleno, jak opravdu funguji javascriptove objekty vzhledem k jeho „skoro prototypicke“ dedicnosti. Rtenka na zrcadle je sice zajimave prirovnani, ale vysvetleni principu copy on write by mozna vrhlo na dany problem vice svetla.

I mechanismus vytvareni objektu je tu popsan trochu nepruhlednym zpusobem, a tim padem zde ani neni zdurazneno, co
zpusobuje za problemy a proc se jimi vubec musime zabyvat.

Zacal bych tedy s vytvarenim objektu pomoci operatoru „new“. Ten nevytvari objekt podle prototype, jak bylo zmineno. Vytvari prazdny objekt pouze s polozkou constructor, do niz se priradi funkce, kterou predavame operatoru new jako parametr. Proto je v javascriptu mozne dat mu funkci, kterou vratime z jine funkce, pole, nebo vlastnosti jineho objektu.

Constructor ma polozku prototype, cili nam v ten moment vznika dulezita vazba object.construc­tor.prototype. Pote se teprve konstruktor zavola tak, jako bychom zavolali Constructor.ap­ply(object, arguments).

Uvnitr konstruktoru a kdekoliv jinde uz se pouziji pouze dve akce. Cteni vlastnosti, nebo zapis vlastnosti. Schvalne je odlisujui, protoze kazda funguje jinak.

1. Zapis vlastnosti (napr. object.name = „Jmeno“; ) – Pokud vlastnost existuje primo na instanci, zmeni se. Jinak se na instanci vytvori, i kdyby existovala na nektereem z predku v retezci prototypu (copy on write).

2. Cteni vlastnosti (napr. alert(object.name); ) – Nejprve se vlastnost hleda primo na instanci. Pokud neexistuje, pres zminenou vazbu object.construc­tor.prototype se vyhleda na predkovi (prototypu), a posleze rekurzivne na vsech predcich, dokud se nenajde, nebo nezjisti, ze neni definovana.

Cili z tohoto je zrejme, ze instance a prototyp jsou nezavisle objekty spojene vazbou „prototypicke“ dedicnosti. To ze jsou nezavisle umoznuje i zmenu tohoto vztahu za behu.

Problem se skills v prikladu je tedy nekde jinde. Ne ve spatnem pouziti, ale spatnem pochopeni vazby. Protoze person.skills je objekt. Konstrukce person.skills­.push(vlastnos­t) je tedy pouziti person.skills pro CTENI, ne pro ZAPIS, a tudiz se nezkopiruje do konkretni instance, ale pracuje se s vlastnosti prototypu.

A ted k problemu dedicnosti. Zpusob, ktery je popisovan v clanku, bych nenazval „sparvnym pro JavaScript“, zpusobem, jak prirozenou JavaScriptovou dedicnost obejit a pomoci closures (zde bych spise pouzil termin „zneuziti closures“, nez u prikladu v prvnim dile) a umele stavby dulezitych vazeb si implementovat svoji. Nic proti tomuto zpusobu, jen bych ho zdaleka neoznacil za „ten spravny“.

Zajimavejsi je, proc se k nemu doslo, tedy, co nam ta Javascriptova dedicnost tropi za problemy?

Je to jednoduche. Mixuje jeden zpusob pro vytvareni prototypu (hierarchie dedicnosti) a vytvareni novych instanci. A problem se objevi tehdy, kdyz mame konstrukcni funkce s parametry, ktere inicializuji vlastnosti objektu.
Vetsinou totiz chceme tyto vlastnosti inicializovat per instance a hlavne v momente vytvareni instance, a ne v momente definovani retezu dedicnosti. Nejmarkantneji se tento problem demonstruje u abstraktnich trid, ktere jeste ve svych konstruktorech volaji delegovane metody, ktere tutiz samy neimplementuji.

Toto neni zadna novinka pro jazyky s protoypickou dedicnosti. Je potreba si uvedomit, ze objekt v roli prototyp ma jiny vyznam, nez objekt v roli instance. Prototyp je objekt, ktery jeste neni inicializovan k obrazu a potrebam instance. Ale existuje. Byl jiz vytvoren. Je tudiz videt, ze existuji dve ruzne dulezite operace. Vytvoreni objektu a inicializace objektu.

Toto je jadro problemu a rozhodne dost dulezite na to, aby bylo v clanku popsano. Nejprirozenejsi a nejjednodussi pristup totiz je, oddelit tyto dva aspekty – konstrukci a inicializaci, a zavest dve dulezite funkce: konstruktor a inicializator.
Tento princip se pouziva v jazycich jako Self, IO, nebo dokonce SmallTalk, ktery je spise tridne instancni (btw. JavaScript byl nejvice inspirovan jazyky SmallTalk a Self).

Vytvoreni instance objektu se tudiz rozpadne na dve operace. Vytvoreni objektu pomoci konstruktoru, a jeho inicializace pomoci inicializatoru. Protoze objekty v hierarchii dedicnosti uz existuje ve forme, v jake je vytvoril konstruktor (protoypy), tak pri vytvareni instance je uz potreba pouze zavolat inicializator predka a ne konstruktor. Jednoduchy priklad:

// Konstruktor
function A() {}

// Inicializator
A.prototype.A = function(name) {
   this.name = name;
   return this;
}

// Konstruktor
function B() {}
// Definice dedicnosti
B.prototype = new A();

// Inicializator
B.prototype.B = function(name) {
    return this.A(name);
}

var b = new B().B("ja");

A voila, mame prirozenou JavaScriptovou dedicnost v plne krase se vsemi vlastnostmi, ktere potrebujeme. Bez zavadeni nejakych slozitych funkci. Jedina vec je, ze jsme rozlisili dve akce, a tudiz vytvorili novou „potrebu“ obe akce volat. Ale pouze pri vytvareni instance.
Tim se meni take pohled na vlastnosti. Nejsou zde staticke a instancni, a tak neni ani dobre takove pojmy zavadet. Jsou zde prototypove a instancni. Jak si navrhneme staticke, pokud je budem nekdy potrebovat, jestli na nekterem prototypu, nebo na konstrukcni funkci, to je na libovuli kazdeho.

Troufnu si tvrdit, ze to co jsem popsal je jediny „prirozeny“ zpusob dedicnosti v JavaScriptu, tim myslim, takhle byla dedicnost pro tento jazyk navrzena. Jestli je „spravny“, nebo „nejlepsi“ v zadnem pripade netvrdim. Stejne tak bych zadne z techto hodnoceni nepouzil pro zpusoby popsane v clanku. Je to osobni preference.

David Grudl

Moc se mi nezdá, že new vytvoří prázdný objekt s položkou constructor. Není to spíš tak, že se položka constructor také (jako jakákoliv jiná položka) čte z prototypu?

kvr

Ale obávám se, že najdete jediný – autor spojil funkci „konstruktoru“ a „initializatoru“ do jedné. Jinak je princip zcela stejný.

Takže oba způsoby jsou ty jediné přirozené. Drobné modifikace, které si autor, Vy, či různé frameworky přidávají, nejsou v tomto směru vůbec podstatné. Použití „jediné správné“ je na místě z toho důvodu, že na rozdíl od jiných přístupů funguje se vším všudy, e.g. včetně korektní funkce instanceof apod.

K rozdílu jenom podotknu, že řešení uvedné v článku má nesporné výhody ve větší přehlednosti, pohodlnosti, kratšímu použití a mj. neduplikaci jména třídy při inicializaci.

K tomu úvodu – je to hodně o slovíčkaření, ale myslím, že i když to není takhle kompletně (a korektně) popsáno, tak je z článku celkem vidět, jak to funguje (viz příklad s delete joe.name). V každém případě, jako doplnění dobré, akorát si nemyslím, že jste s autorem nějak ve sporu ;)

Přeskočím na konec, ke statickým vlastnostem – jde čistě o názvosloví, autor používá to známé s C++ či Javy. Asi se shodnem, že definovat statické vlastnosti v rámci prototype je sice teoreticky možné, ale opět – nepohodlné, přinejmenším se musí psát jedno slovo navíc.

PS: Příklad se skills není zrovna šťastný ani v článku, ani v komentáři. V prvé řadě to totiž selže kvůli tomu, že na rozdíl od některých jiných jazyků (např. magické PHP), se pole chová konsistentně s ostatními objekty a přístup k němu či přiřazení do jiné proměnné nekopíruje jeho obsah, ale pouze referenci. Tzn. i kdyby bylo v konstruktoru this.skills = this.skills, nebo třeba this.more_skills = this.skills, tak se stále bude modifikovat pole, které je přiřazeno do prototype.
Mimochodem, při zápisu instanční proměnné se nikde technika copy on write neuplatňuje, pouze se vytváří nová položka v instanci (či přemaže stará).

v6ak

Ke konzistentnímu chování: abych to upřesnil, neznamená to to, že by v PHP taková věc byla bezproblémová kvůli nekonzistentnosti, ale kvůli jiné logice OOP.

Copy on write je IMHO mnohdy čistě implementační detail a odkrýt jej nebývá jen tak a vyžaduje často určité hacky: http://gist.github.com/321601 . A JS má mnohem víc implementací.

Ondřej Žára

1) Operátor new vytváří objekt přesně podle prototype
Tohle je prostě jasně zavádějící a matoucí formulace. Je mi líto, že se musím opět ohánět specifikací, ale její výklad je v tuhle chvíli vyznamně přesnější a zároveň ilustrativnější. Operátor new vyrobí úplně normální běžný tuctový regulérní klasický prázdný objekt (stejně jako {}), jako bonus mu nastaví skrytou vlastnost a na závěr nad ním zavolá funkci konstruktoru.

2) O tom, že by constructor byla „přepsaná“ vlastnost, bych si dovolil pochybovat. Constructor je vlastnost nadefinovaná (s příznakem DontEnum) v prototypovém objektu, viz tato pozorování:

var A = function() {};
var a = new A();
a.hasOwnProperty("constructor"); // false
A.prototype.hasOwnProperty("constructor"); // true

var B = function() {};
B.prototype = new A();
var b = new B();
b.constructor == A; // true

4) Nemám co dodat, snad jen, že váš způsob nepoužívá vůbec nikdo.
Škoda takové útočné a dehonestující formulace. Ohánění se zobecněními je pod úroveň pisatele odborného článku. Osobně mi zmíněná striktní separace konstruktoru a inicializátoru také nevyhovuje, ale abych si dovolil pisatele zesměšňovat (nedokazatelným) tvrzením, že jeho postup vůbec nikdo nepoužívá…

Michal Augustýn

1) Zde se musím zastat Ondřeje. new opravdu vytváří obyčejný prázdný objekt, tak to prostě je (viz specifikace nebo třeba implementace ve SpiderMonkey/Tra­ceMonkey). Není jednoduché to dokázat, protože při čtení property a jejím nenalezení na instanci to automaticky fallbackne do prototype.

3) Copy on write není myšleno jako záležitost implementace, ale jako způsob, jakým se přistupuje k properties. Aneb při readu to fallbackuje do prototypu, při writu to vždy zapíše do instance (při přiřazení se prototype vůbec neuplatní).
Viz můj (já vím, nedokonalý) článek, konkrétně část „Prototypy“.

Michal Augustýn

1) Jo, máš pravdu, že z hlediska běžného uživatele Javascriptu můžeme tvrdit, že new vytvoří objekt stejný jako prototype.
Ale když se programátor začne zajímat třeba o paměťovou náročnost jeho řešení, tak tenhle implementační detail vyplave na povrch – pak je totiž diametrální rozdíl, jestli si drží 1000000 instancí svou kopii nějaké property nebo všechny instance sdílí jednu a tu samou property z prototype (samozřejmě do okamžiku zápisu).

David Grudl

Mám pocit, že tady mícháte dvě docela odlišné věci.

Prototype & prázdný objekt: to je velmi důležité chování JavaScriptu, i z hlediska „běžného“ programátora, bez něj totiž stěží prototype pochopí. Tudíž mi v článku zmínka o něm citelně chybí. A jde samozřejmě o „dokazatelnou“ věc, viz příklad s hasOwnProper­ty níže.

Naopak Copy on write je implementační detail, zejména u objektových jazyků téměř nepodstatný a programátora nemusí vůbec zajímat.

Michal Augustýn

Ha, výborně, na hasOwnProperty jsem úplně zapomněl :)

junix

1) S formulaci nesouhlasim z toho duvodu, ze pod ni rozumim to, ze novy objekt, ktery vytvarim podle jineho, bude mit vsechny polozky zkopirovane. Tudiz, pokud se puvodni objekt zmeni, ten novy se nezmeni. To ale pravda neni. Pokud zmenim vlastnost prototypu, projevi se zmena i v potomcich.

(ja pro zmenu vynechavam bod 2 ;) )

3) Copy on write je prave dulezity princip ze stejneho duvodu, jako chybne tvrzeni v bode 1.
Dokud totiz instance neobsahuje vlastnost primo v sobe, ale v nekterem objektu v retezu prototypu, take se projevi, pokud se tato vlastnost na danem prototypu zmeni.

4) Cekal jsem spise argumenty, ze je toto reseni pracnejsi, nebo z nejakeho hlediska horsi, a ne hned takove definitivni a navic mylne zavery o pouzivani ve „veskerem javascriptovem svete“ ;)

Nicmene rad znovu objasnim, proc je tak dulezite, alespon pro me. Protoze je to nativni zpusob, dany JavaScriptu do vinku primo jeho tvurci. Nepotrebuji k jeho fungovani zadne magicke funkce extends, zadne umele vytvareni anonymni konstrukcni funkce. Je to sice za cenu rozdeleni konstruktoru a inicializatoru, coz ovsem rozhodne neni nic proti cistote reseni, ale ma jednu nespornou vyhodu.
Kdyz vyvojar znaly JavaScrptu prijde ke kodu, ktery vyuziva nejakou vlastni implementaci dedicnosti (napr. funkce extends), jeho prvni problem bude, ze neuvidi na prvni pohled, jakym zpusobem se dedicnost realizuje. Jestli je to vas prvni, druhy, treti, nebo uplne jiny zpusob, jestli je to opravdova dedicnost pomoci prototype, nebo jen volani funkci, ktere postupne skladaji objekt?
Ve zpusobu, ktery jsem popsal, je toto naprosto jasne, a staci znalost jazyka. A prave proto bych od clanku s nazvem „Třídy, dědičnost a OOP v Javascriptu“ cekal prave ten nativni pristup. A vsechny ostatni bych uvedl az v kapitolach/dilech „Jak si javascriptovou dedicnost usnadnit/upra­vit/vylepsit“.

Javascript nam dava velkou volnost v tom, jak ho budeme pouzivat. Ale kdyz ji chceme vyuzit, nemuzeme zacit od konce ;)

junix

1) a 3) Ja si prave myslim, ze toto by melo byt receno nejak vice explicitne. Netvrdim, ze clovek, ktery alespon neco tusi, si to v prikladech a prirovnanich v clanku nenajde.

„A vysvětlil jednotlivé kroky. A myslím, že tak podrobně, že mi nikdo nemůže říct, že jsem něco podstatného zatajil ;)“

S touto vetou bych si prave dovolil nesouhlasit. Pokusim se nastinit proc, a co je pro me zasadni problem predstavene funkce extends. Pro poradek ji zde jeste jednou uvedu a doplnim sve komentare:

// Funkce extends, od ktere ocekavame, ze vytvori vazbu
// dedicnosti mezi child a parent.
// Ta by mela vypadat v JS tak, ze do child.prototype se priradi
// objekt vytvoreny pomoci parent, napr.
// child.prototype = new parent();
//
var extends = function(child, parent) {
    // F je pomocný dočasný konstruktor
    // OK, tady zadny problem.
    var F = function() {};
    // Tady se vytvari vazba mezi prototypem nove funkce
    // a prototypem parenta. Nikoliv parentem primo.
    F.prototype = parent.prototype;
    // Zde vytvarime vazbu dedicnosti, ale open ne s parentem
    // ale s docasnou funkci, ktera ale take nema vazbu primo
    // s parentem, pouze s jeho prototypem.
    child.prototype = new F();
    // konvence pro volání přepsaných metod
    // Toto neni jiz podstatne, spis zavadi novy umely vztah.
    child._superClass = parent.prototype;
    // dobrým zvykem je, aby instance odkazovala na svůj konstruktor
    // Take nepodstatne pro moje vysvetleni
    child.prototype.constructor = child;
// Ted jsme na konci, a stale se nevytvorila vazba mezi child a parent!!
};

Mozna jsem jediny ignorant, kteremu unika neco zakladniho. Rozhodne neco, co v clanku zmineno neni, a treba je takovych ignorantu jako ja vice. Ale jak to vubec funguje, kdyz se v teto funkci vazba dedicnosti mezi child a parent nevytvorila?

Funguje. To si muzeme vyzkouset. A tak nezbyva, nez si vice lamat hlavu nad tim, jak je to mozne. Tedy predevsim, jak to, ze funguje dedeni a jak to, ze funguje operator instanceof.

Zkusim assci art pro nastineni problemu, co vznikne po pouziti funkce extends:

        parent.prototype (object(Object))
         |                       |
         |                       |
  f (object(Function))     parent (object(Function))
         |
         |
 child.prototype (object(f))
         |
         |
  child (object(Function))

a nasledne muzeme vytvorit instanci pro testovani

         |
  instance (object(child))

Z obrazku je videt, ze sama funkce parent stoji MIMO prototype chain objektu vytvoreneho pomoci funkce child.

A ted pro to tedy funguje. Dedicnost je jasna. Primo funkce neobsahuje metody ani vlastnosti, ktere by objekt pouzival. Cili pri vyhledavani jdeme pouze po prototypech. Protoze prototyp funkce parent uz v retezu je, pak se dostaneme na vlastnosti, ktere jsme mu dali, a tim padem to vypada, jako by byl parent sam v retezu dedicnosti. Stale upozornuji, ze neni.

A co operator instanceof? Take zafunguje. Diky necemu, co bych si ja dovolil oznacit za chybu specifikace. Ten totiz podle specifikace funguje tak, ze porovnava onen zmineny vnitrni ukazatel objektu na parenta (nekdy oznacovany __proto__, ve specifikaci pouze nepristupny [[Prototype]]) s atributem prototype funkce, ktera je druhym argumentem operatoru. Cili neoveruje, ze prave testovana funkce je v retezu prototypu, ale jeji prototype (uz je to trochu komplikovane, co? :) ). V praxi to znamena, ze tento operator vam vrati true, i kdyz vas objekt NENI potomkem testovane funkce. Cili neni spolehlivy, vlastne funguje spatne.

Takze muj zaver je: operator instanceof je chybne specifikovan. Funkce extends teto chyby zneuziva. Ve skutecnosti nevytvari vztah dedicnosti mezi child a parent, ale obalamuti tento operator a dalsi javascriptove mechanismy, aby se tak chovaly.

Muj osobni nazor je, ze toto reseni je hack, a neomlouva ho ani to, ze ho pouziva Google, nebo Yahoo ve svych knihovnach ;)

Nyni samozrejme ocekavam namitky, ze je jedno, jestli se jedna, nebo nejedna o „skutecnou“ vazbu mezi child a parent, kdyz se to ve vsech aspektech chova, jako by existovala, a nebudu mit, co proti tomu rict. To ani nebyl ucel. Ten byl:

Me uz k pochopeni, proc funkce extends funguje, chybela v clanku zasadni informace. A bez jejiho radneho pochopeni jsem k ni musel pojmout neduveru (to by predpokladam udelal kazdy, pokud neco presne nechape).

Ted uz ji chapu, a pro zmenu se mi nelibi princip, ale to uz je pouze moje osobni preference ;)

Ondřej Žára

Takze muj zaver je: operator instanceof je chybne specifikovan.

To je silné tvrzení, rozhodovat o správnosti / nesprávnosti specifikace. Faktem zůstává, že operátor instanceof ve specifikaci je, všechny interprety ho implementují shodně a programátoři ho využívají. Přijde mi proto velmi přirozené psát kód, který tento operátor respektuje.

Vítejte tímto ve světě prototypové dědičnosti, kde jedna instance splňuje test instanceof pro celou řadu různých funkcí, které spolu mají jedinou věc společnou – totiž prototypový objekt. Osobně v tom nevidím pražádný problém.

V předchozím příspěvku je několikrát správně zmíněn klíčový fakt, totiž že neexistuje žádná pevná vazba mezi „instancí“ a „konstruktorem“. Skutečně, prototypový řetězec jde bokem a funkce konstruktoru je z něj odkazovaná jen dost slabou (a jak bylo již několikrát ukázáno, pochybnou) vlastností „constructor“.

Mimochodem, řešení s odděleným konstruktorem a inicializátorem se chová navlas stejně. Píšete A voila, mame prirozenou JavaScriptovou dedicnost v plne krase se vsemi vlastnostmi, ktere potrebujeme, ale vazba mezi instancí b a funkcemi A i B je zcela stejná, jako při použití extends.

kvr

+1

Nemám takový přehled, abych tvrdil něco o špatné specifikaci :), takže ke zbytku:

Berte prototyp jako definici třídy (což v podstatě je) a Person = function(name) { this.name = name; } jako konstruktor, pak tento přístup začne dávat logiku.

Ještě k instanceof – to, že (viz výše) pracuje s prototypem (třídou) je zcela logické, stejně jako v C++ funguje  class A {}; typedef A B; dynamic_cast
(new A)
.

K tomu, že v kódu při explicitním rozlišení konstruktoru a inicializátoru vidíte, co se děje pod pokličkou – to přece nikoho nezajímá. Když komunikujete s databází, tak taky začnete posílat bajty na IO síťové karty, či když řídíte auto, tak přepínáte proud na svíčky? Co jste uvedl, je dobré pro pochopení principu dědičnosti, ale nemusí být nutně dobré pro práci.

PS: Last but not least – zjistěte si, co znamená Copy on write, znovu opakuju, že ho používáte ve zcela nesmyslné souvislosti.

junix

Ale s tim preci souhlasim, ze to co se ve skutecnosti „dedi“, cili neco jako trida, je prave ten prototyp, a ne konstrukcni funkce. O tom se nepru. Poukazuji na to, ze operator instanceof prave NEPRACUJE primo s prototypem. Pracuje s nim skryte. Az pote, co se k nemu dostane z funkce, kterou jsme mu zadali jako operand. To by bylo v poradku, kdyby platilo, ze kazda funkce ma svuj unikatni prototype. To ale jazyk primo nezajistuje. A proto zde uz ze specifikace vznika konflikt. A to je rozhodne nedostatek specifikace. Nebo mi chcete stale tvrdit opak? Muzu konfliktu predejit doplnenim sveho pristupu o umele konvence a jejich pouzivanim. To je tak vse.

Dale bych ocenil, kdybyste si uvedomil, k cemu slouzi takove clanky, jako tento. Tedy ke vzdelavani. Ano, pokud budu chtit sitovou kartu pouzivat, koupim ji, zapojim, nainstaluji ovladac a vic me nezajima. Pokud si budu cist odborny clanek o sitovych kartach, ktery se bude primo jmenovat IO sitove karty XY, pak budu cekat, ze tam prave informace o posilani bajtu a jejich vyznamu bude.
Tudiz, kdyz ctu clanek o dedicnosti v JavaScriptu, ctu ho s tim, ze se zde dozvim, na jakych principech funguje. Pokud vy ne, pak nechapu, proc jste clanek cetl cely, vcetne diskuse, a nevzal si z nej pouze funkci „extends“ ;)

Omlouvam se predevsim autorovi clanku, ze jsem se ted nechal unest skoro az k osobnimu utoku a mohlo by se zdat, ze se mi clanek vubec nelibil a ze v nem neni vysvetleno nic. Tak to neni. Vetsina veci je v nem vysvetlena pekne. Jen mi jedna zasadni vec chybela a druha mi prisla v textu popsana ne dost vystizne.

junix

K osobnimu utoku na kvr kvr, ne na autora

kvr

Ad 1: Ok, částečně souhlasím, instanceof bere jako operand trochu nelogicky konstruktor. Nicméně s konvencemi bych to neviděl tak tragicky, ten „správný“ zápis je dost přímočarý a to, že jazyk dává možnost některé věci přiohnout, bych neviděl vždy jako zápor. Samozřejmě, když se toho chytne nějaký hacker s přehnaným sebevědomím, může to dopadnout špatně, ale to je holt všude…

Ad 2: Narážel jsem na tohle:
Muj osobni nazor je, ze cistsi je to, co je jasne a plne pod programatorovou kontrolou. Cili kdyz jasne vidim, co se deje pri konstrukkci prototypu, nebo jineho objektu, a co se deje pri jeho inicializaci. Cokoliv, co mi tento princip skryva a oddaluje, je spis hack.
Pokud jste to myslel pouze v rámci toho, že to mělo být uvedeno v článku, pak ok, souhlasím. Ale chápal jsem to, jako obecný příklad do praxe.
2.2: Funkci extends (no spíš něco daleko silnějšího) jsem si napsal před půl rokem, kdy jsem se začal JS zabývat. Článek i diskusi beru jako užitečnou věc v rožšiřování rozhledu, kde byl i přes některé chyby přínosem i Váš první příspěvek.

Ad 3: Osobní útoky jsem nezaznamenal a doufám, že ani ne z mé strany…

junix

Ad 2: Aha, ja zas nabyl dojmu, ze to podle vas nepatri ani do clanku. V tom pripade jsme za jedno. V clanku by to podle me byt vysvetleno melo, ale v praxi si kazdy vybere, co mu nejlepe vyhovuje.

Ad 2.2: Rozsirovani rozhledu se rozhodne clanku ani diskusi neda uprit. Me osobne se nektera zakouti JavaScriptu vyjasnila, at uz diky prizpevkum v diskusi, nebo dohledanim ve specifikaci a jinych materialech, obcas i se zjistenim, ze jsem se take v leccems mylil.

junix

Ano souhlasim se vsim :)

Tvrzeni o spravnosti bylo silne, nebo spis spatne.

Problem je v tom, ze javascript nema ciste prototypickou dedicnost, coz zpusobuje, ze se v tom clovek ztrati.

V ciste prototypicke dedicnosti vytvarim vztavy primo mezi objekty (napr. v IO pomoci Object.clone), a pokud se budu ptat na neco jako „instanceof“, budou oba parametry objekt. V tom neni problem.

Jenze v JavaScriptu se instanceof pta ne objektu, ale funkce, ktera ovsem jaksi v retezci prototypu neni. Jak spravne pisete (a ja psal v prvnim komentari spatne), objekt nema po vytvoreni vazbu na funkci, kterou byl vytvoren, ale pouze na jeji prototyp. A tak se vlastni testovani musi na objekty prevest, coz zmineny operator udela pomoci vyhledani prototypu funkce. No a to je zdrojem meho zmatku (mozna ze by se naslo vice takto zmatenych programatoru, kteri by ocenili vysvetleni tohoto principu pred jeho pouzitim).

A mate pravdu a ja ani nikde netvrdil, ze funkce exteds a prime prirazovani prototypu se chova jinak. Chova se stejne. Ale rozdil zde je. Bohuzel se fyzicky vytrati, a zbyde jen semanticky, tudiz nerozlisitelne. Kdyz se podivate do meho predchoziho prizpevku na acsii art graf :) bude v pripade primeho prirazeni parent (object(Function)) na miste f(object(Functi­on)), cili BUDE v primem retezu vytvareni, bohuzel ne prototypu. Znamena to, ze prototyp byl opravdu vytvoren touto funkci. A to, ze se tato vazba vytrati, opravdu povazuji za slabe misto primo jazyka.

Michal Augustýn

Myslím, že si špatně vysvětlujete účel funkce extends. Vy píšete „Funkce extends, od ktere ocekavame, ze vytvori vazbu dedicnosti mezi child a parent.“, kdežto Dainel píše ve článku „Nic nám nebrání podědit prototype, třeba pomocí takovéto funkce“.

Tedy účel funkce extends je jen provázat prototypy. O správnou inicializaci instančních členů se postará volání Person.call(this, name);.

K tomu vašemu řešení. Dejme tomu, že mám nativní třídu File, která vyžaduje, aby měl konstruktor (minimálně) jeden parametr, který obsahuje jméno souboru k otevření, jinak failne.
Pak Vaše volání „child.prototype = new parent();“, příp. „B.prototype = new A();“ v definici dědičnosti taky failne.
Samozřejmě pokud dojde k rozdělení na konstruktor a inicializátor, tak tenhle problém odpadne, ale toto rozdělení se mi vůbec nelíbí, protože programátor je při vytváření instancí nucen používat dost nepřirozenou syntaxi (to je to jediné, co se mi na Vašem přístupu nelíbí, ale je to pro mě dost zásadní).

junix

Ano, tohle je argument na miste: „ale toto rozdělení se mi vůbec nelíbí…“ a takove jsem cekal spis.

„Tohle bych nepouzil, protoze je to vic psani, protoze bych musel pouzivat konstrukce, na ktere nejsem zvykly, protoze…“

Ja bych zas nepouzil fci extends, protoze skryva skutecny vztah, nebo protoze vyuziva vedlejsich efektu, nebo protoze… :)

A z toho lze vyvodit jedine. Zadny z techto zpusobu neni „lepsi“ nez ten druhy a tudiz „ten spravny“. Je vzdy lepsi v necem. A tak bychom se meli zdrzet takoveho hodnoceni a zustat u osobnich preferenci ;)

Nebo s tim nesouhlasite?

David Grudl

ad 1) zkusit dokázat opak? To je poměrně snadné:

var Animal = function(name) {
    this.name = name;
};

Animal.prototype = {
    getName: function() {
        return this.name;
    }
};

var pes = new Animal('pes');
alert(pes.hasOwnProperty('getName')); // vraci false
MD

Proc nikdo neodpovi? :-)

olin

Na přání mu bylo dokázáno – k tomu nelze už nic dodat. Maximálně omluvu za silná tvrzení. I z komentářů k minulému článku mi to ale připadá, jakoby Dan (promiň, Dane) hledal mermomocí argumenty pouze pro svá tvrzení, aniž by se snažil zjistit, proč někdo tvrdí opak. Ono přesně tyhle výroky, které Dan má, rozhodně nezvyšují jeho věrohodnost – a to zvláště poté, co se vyvrátí.

Tím se nesnažím potopit Dana, určitě má za sebou spoustu projektů a zkušeností, ale je to pouze doporučení pro příště ohledně vyjadřování – více ověřovat, méně tvrdit.

Michal Augustýn

Na Vaší ukázce dědičnosti je blbé to, že v definici dědičnosti se volá new A(). Pokud je A nějaký můj nativní objekt, který třeba otevírá file handle, tak je to pěkně na p…
Daniel ve článku dělá prakticky to samé, co Vy, pouze se vyhýbá výše zmíněnému volání new A(). Místo toho vytváří pomocnou konstrukční funkci a vytváří pomocí ní instanci, což je IMHO čistčí…

junix

Myslim, ze jsem spise zvolil nevhodne shodne nazvy konstruktoru a inicializatoru. Pokud dam inicializatorum prefix init (tedy initA a initB), pak snadneji ukazu, ze v tomto neni mezi obema resenimi rozdil. Jen je potreba chapat posun vyznamu.
V Danielove prikladu je anonymni funkce konstruktor, a funkce, kterou definuje jako „tridu“ je jen inicializator.
V mem prikladu je konstruktor funkce A a inicializator funkce initA. Vas filehandle muze byt instancni, nebo prototypova vlastnost, a podle toho jej pouze umistite do prislusne funkce. Pocitam, ze jste mel na mysli instancni, cili umistit do initA.
Navic tu ale mate moznost prototypove vlastnosti inicializovat v konstruktoru, coz naopak v Danielove prikladu vubec nemate.

BTW. nevidim ani v cem je vytvareni pomocne anonymni funkce cistsi? Muj osobni nazor je, ze cistsi je to, co je jasne a plne pod programatorovou kontrolou. Cili kdyz jasne vidim, co se deje pri konstrukkci prototypu, nebo jineho objektu, a co se deje pri jeho inicializaci. Cokoliv, co mi tento princip skryva a oddaluje, je spis hack.

Michal Augustýn

Navic tu ale mate moznost prototypove vlastnosti inicializovat v konstruktoru, coz naopak v Danielove prikladu vubec nemate.

Vůbec nevím, jaké by mělo takové počínání smysl :)

keff

Děkuji panu Steigerwaldovi za imho jeden z nejlepších českých článků o javascriptu.

Zároveň bych chtěl odkázat na neméně výborný článek Steva Yegeho: The Universal Design Pattern http://steve-yegge.blogspot.com/2008/10/universal-design-pattern.html, který nás čtivě provede ideovými základy prototype systému, a ukáže jeho cestu napříč různými jazyky a prostředími.

David Grudl

Trošku odbočím: nevíte, proč nejde v IE vytvářet objekty, jejichž prototypem je DOM element? Např:

var F = function() { };
F.prototype = document.getElementById('name');

var f = new F();
alert(f.tagName); // vrací undefined
David Grudl

Jasně, ale to odpovídáš na jinou otázku. Použití F.prototype = document.getE­lementById(‚na­me‘)

mi nefunguje ani v IE 8.

junix

Jake by bylo dalsi pouziti objektu vytvoreneho pomoci new F()?

Aleš Roubíček

Doporučuju mrknout na tohle video http://live.visitmix.com/MIX10/Sessions/CL29 někde v části, kde se probírá Marshaling (kolem 30. minuty) je to celkem slušně vysvětleno.

junix

Pomerne hezky clanek je take zde:
http://joost.zeekat.nl/constructors-considered-mildly-confusing.html
Je sice zameren jen na dilci problem vlastnosti constructor, ale doplnen peknymi grafy pekne ilustrujicimi diskutovane vztahy objektu a konstrukcnich fci.

tuft

Existuje ještě další přístup k implementaci „dědičnosti“:

function parent() {}
parent.prototype.f = function () {};
...

function child() {}
for (var method in parent.prototype) {
   child.prototype[method] = parent.prototype[method];
}

Tento postup nevytváří vazbu mezi parent – child, ale funkce f je mezi nimi sdílena, dokud není na jednom z nich přepsána. Pokud nechceme měnit prototypy za běhu skriptu a dědíme velké množství objektů, tak bych použil právě tento postup, protože volání metod vzdálených rodičů zde bude rychlejší.

Co si o tomto postupu myslíte?

Michal Augustýn

Možností, jak implementovat dědičnost v JavaScriptu existuje nespočet a další budou jistě předvedeny v dalším díle článku.

Vámi uvedené řešení bych dnes asi spíš nepoužil (přitom ho sám popisuju ve svém článku, ale člověk se stále učí ;-)) a dal bych přednost implementaci z článku.

To proto, že v JavaScriptu nebudou už properties objektu jen tupé zapisovače/vkládače hodnot, ale bude možné specifikovat gettery a settery, tedy zapisovací a čtecí funkci pro každou property. Ve Vaší implementaci by se pak nakopírovaly získané hodnoty properties, ne gettovací/settovací funkce.

Výše uvedené gettery a settery jsou dostupné už teď v TraceMonkey/Spi­derMonkey (__defineGetter__, __defineSetter__).
Připravovaná specifikace nového ECMAScriptu ale přistupuje k definici getterů a setterů trošku jinak. A podle tohoto nového standardu to implementuje IE8, bohužel ale jen pro DOM objekty.

Zkrátka a dobře, kopírování properties bych se při širším použití na webu vyhnul; v nějakých specifických nasazení to může mít ale své opodstatnění.

kvr

Tak, modifikaci prototype zaživa považuju stejně celkem za prasárnu, ani ji nepovažuju za důležitou pro definici dědičnosti, takže proti tomu (možná kromě specifikace) nic.

Ta implementace má ale jiný problém – nebude fungovat operátor instanceof, a to je pro dědičnost problém dost zásadní…

Ondřej Žára

Presně tak, rozbití instanceof je největší slabinou této realizace.

Dále zde vidím otravnou nutnost zmíněný cyklus vykonávat pro všechny třídy, které mají rodiče. Řekl bych, že je to díky tomu i výkonově horší.

Uvedený příklad je ale hezkým řešením pro situace, kdy chceme napodobit vícenásobnou dědičnost, resp. rozhraní. V takové chvíli nepotřebujeme fungující instanceof a předvedená varianta by se dala použít.

Michal Augustýn

Přesně tak – vícenásobná dědičnost je přesně ten speciální případ, kdy (asi) nic jiného než kopírování properties nezbývá.

Michal Augustýn

Ale fuj, měnit prototype zaživa se mi také nelíbí ;-) Je k tomu nějaký praktický důvod? Přijde mi přirozené a přehledné mít kód rozdělený do deklarační a výkonné části…

keff

Nedalo mi :)

„In theory there is no difference between theory and practice. In practice there is.“

Poetika na úvod je božíí :))

junix

Ano, duvody existuji :) Staci se podivat, co je ten nejzasadnejsi princip v OOP. Tim je polymorfismus. Ten ma ruzne formy a na nektere se dost zapomina. K cemu slouzi?

Casto je za polymorfismus povazovana jedna z jeho forem. Nazveme ji typovy/tridni polymorfismus. Znamena, ze objekt odpovi na poslanou zpravu podle toho, „jaky je“ (k jake tride patri, ci je potomek, jakou ma implementaci metody).

Polymorfismus ale neni jen tato forma. Ma jich vice. Dalsi dulezita forma je „Stavovy polymorfismus“. Objekt odpovi na poslanou zpravu podle toho, „v jakem je stavu“. V javascriptu mame paradni moznost zmenu stavu provazet prave zmenou chovani – zmenit za behu metodu.

No a ohromne kouzlo je prave v tom, ze pokud to udelame na prototypu, pak jednoduse zmenime chovani cele skupiny objektu (vsech potomku).
Dalsi velke kouzlo je, ze muzete pro jeden objekt, nebo tridu zmenit chovani oproti ostatnim, a pak se zase vratit (nastaveni a nasledny delete pro metodu, kterou obsahuje i prototyp).

Jdou s tim delat hotove cary.

tuft

Proto jsem dal dědičnost do uvozovek (nevím jestli je nějaká přesná definice dědičnosti, jestli ano, skoro bych si tipnul, že by se do ní uvedený způsob vešel..). Napsal jsem, že je to použitelné pokud nechceme za běhu měnit prototypy objektů, což přesně dělá Váš příklad ;-). Jinak nechci nijak tento způsob hájit, sám ho nepoužívám, ten ukázaný v článku je určitě v 99% případů lepší.

tomFlidr

Ahoj,

velice mě potěšilo když jsem článek četl, včetně komentářů.
Chtěl bych se vás ale zeptat na jinou věc.

Netlačí mě ani tak dědičnost nebo ona praktická věc – netvořit třídy, které by měli „privátní“ proměnné (I. díl) a spíš než to bych správně používal prototype abych tyto proměnné či další metody mohl odkudkoli měnit apod…

Když něco v JS píši, spíš než tyto věci mi ve většině případů vyvstává potřeba mít objekt/třídu/fci, kterou bych mohl vytvořit bez toho, aniž bych dopředu věděl kolik jich bude a tyto mé instance se vždy dokázali postarat sami o sebe s tím co mají za hodnoty, bez ohledu na to co je v nejvyšším namespace, i co se týče postarání se o objekty v DOM, podle toho kteréžto CSS selectory jim byli předány k práci – příklad – rozbalovací horizontální menu (<del>http://­www2.studenta­gency.cz/?node=1193</del­&gt;).

K věci – jak vytvořit způsobem, který tu byl pospán jako nejlepší cesta třídu, která by uměla řešit volání své vnitřní metody v intervalu? Se svými hodnotami proměných?

Článek k problému najdete třeba zde:
http://www.vonloesch.de/node/32

Jak to zapsat jinak (pomocí prototype) než pomocí způsobu z prvního článku, který je trochu add hoc řešení, ale přesto se v praxi pro tuto potřebu ukázal jako best way?
mluvím o tomto:

var Menu = function(selector){
  var counter = 0;
  var intervalId;
  var intervalCall = function(){
    if(counter < 5){
      counter++;
      alert(selector + counter)
    }else{
      clearInterval(intervalId)
    }
  };
  return {
    process: function(){
      intervalId = setInterval(intervalCall, 500)
    }
  }
};
var firstMenuObj = new Menu('.first');
firstMenuObj.process();
var secondMenuObj = new Menu('.second');
secondMenuObj.process(); 

Díky, tf.

tomFlidr

Pane jo, vy jste to s tím kontextem vlastně vyřešil úplně elegantně:)
Příště budu snad už přemýšlet podobně.
Používám zatím jen jQuery, pomalu pročichávám Google Closure, ale jde to pomaleji než ten před tím. Každopádně díky moc za vaši dobrou radu.
A taky bych řek, že od teď už vlastně nemám potřebu psát kód v JS pomocí přístupů, které jste zmiňoval jako zlé.
Nakonec se mi tím otvírá daleko více možností.
Děkuji vám.
Kde vám můžu přidat karmu?

v6ak

Pod komentářem je + a -.

tomFlidr

:) jojo, záhy jsem je pak viděl.
Budu se tešit na další článek:-)

Michal Aichinger

Možná by stálo zmínit, že v ES5 je metoda bind standardizovaná. Najdeme ji v prototypovém objektu Function. Tudíž bychom mohli psát v metodě process udělat něco takového:

this.intervalCall = this.intervalCall.bind(this);
this.intervalId = setInterval(this.intervalCall, 500);
nikdo

Ano, jsem si vědom, že píši komentář téměř 2 roky po vydání článku :)

‚extends‘ je slovo rezervované pro případné použití v budoucnu a nelze jej tedy použít jako identifikátor (možná před těmi dvěma lety ještě nebylo, nevím, nejsem expert na historii JS) – viz třeba referenční příručku – https://developer.mozilla.org/en/JavaScript/Reference/Reserved_Words#Words_reserved_for_possible_future_use (na kterou je ostatně odkaz v prvním dílu seriálu)

Takže ani poslední příklad na jsFiddle nefunguje (asi původně fungoval, že si toho autor nevšiml).

Enum a statická analýza kódu

Mám jednu univerzální radu pro začínající programátorty. V učení sice neexistují rychlé zkratky, ovšem tuhle radu můžete snadno začít používat a zrychlit tak tempo učení. Tou tajemnou ingrediencí je statická analýza kódu. Ukážeme si to na příkladu enum.