JS Objects:De”konstrukce”.

Objekty JS:TL;DR

JavaScript byl od začátku sužován nedorozuměním a neohrabaností kolem svého systému „prototypové dědičnosti“, většinou kvůli skutečnosti, že „dědičnost“ vůbec není způsob, jakým JS funguje, a snaha o to vede pouze k nedorozuměním a zmatkům. musí vydláždit pomocnými knihovnami uživatelské země. Místo toho přijetí toho, že JS má „delegování chování“ (pouze delegování odkazů mezi objekty), přirozeně zapadá do toho, jak funguje syntaxe JS, což vytváří rozumnější kód bez potřeby pomocníků.

Když dáte stranou rušivé prvky, jako jsou mixiny, polymorfismus, kompozice, třídy, konstruktory a instance, a soustředíte se pouze na objekty, které na sebe navazují, získáte mocný nástroj v delegování chování, o kterém se snadněji píše, uvažuje, vysvětluje, a udržovat kód. Jednodušší je lepší. JS je "pouze objekty" (OO). Nechte výuku těm jiným jazykům!

Děkuji

Rád bych poděkoval následujícím úžasným vývojářům za jejich štědrý čas při zpětné vazbě/technické recenzi této série článků:David Bruant, Hugh Wood, Mark Trostler a Mark McDonnell. Jsem také poctěn, že David Walsh chtěl publikovat tyto články na svém fantastickém blogu.

Úplná série

  • Část 1:Objekty JS:Zděděný nepořádek
  • Část 2:Objekty JS:Rozptylování
  • Část 3:Objekty JS:De"konstrukce"

V 1. části této série článků (kterou byste si měli úplně přečíst, pokud jste to ještě neudělali!), jsem se vrátil k myšlence, která pro mě není originální: JS nemá „dědičnost“ v tradičním slova smyslu a co má je vhodnější označení „delegování chování“ – schopnost jednoho objektu delegovat přístup k metodě nebo vlastnosti, kterou nemůže přenést na jiný objekt, který může zvládnout to.

Poté jsem se v části 2 věnoval několika rušivým vlivům, které myslím zatemnit skutečnou objektově orientovanou identitu JS, včetně „vlastních typů“, „mixinů“, „polymorfismu“ (ke kterému se ještě vrátíme později) a dokonce i nové „syntaxe tříd“ přicházející v ES6. Navrhl jsem to pro lepší pochopení (a využití) [[Prototype]] , potřebovali jsme svléknout cruft. Tady se o to pokusím.

Želvy Objekty zcela dolů nahoru

Klíčovým zjištěním, pointou celé této série článků, je, že [[Prototype]] je ve skutečnosti pouze o propojení jednoho objektu s jiným objektem pro účely delegování, pokud první objekt nemůže zpracovat přístup k vlastnosti nebo metodě , ale druhý ano. Jinými slovy, jsou to pouze objekty spojené s jinými objekty. To je opravdu vše, co JS má.

V jistém smyslu je JS nejčistší podstatou „objektově orientovaného (OO)“ jazyka, a to v tom, že skutečně je vše o předmětech. Na rozdíl od většiny ostatních jazyků je JS poněkud unikátní v tom, že můžete skutečně vytvářet objekty přímo bez pojmu tříd nebo jiných abstrakcí. To je výkonná a skvělá funkce!

JavaScript legitimně je „objektově orientovaný“ a možná jsme tento termín neměli používat pro ostatní jazyky, které znamenají mnohem více než jen „předměty“. Možná by bylo přesnější „orientované na třídu“, což by nás uvolnilo k použití „objektově orientovaného“ pro JS. Samozřejmě, jak jsem tvrdil v části 1, záleží na tom, co každý myslí, když použije nějaký termín, takže je příliš pozdě na to, abych předefinoval nebo ohýbal běžně přijímané „objektově orientované“ pro své vlastní účely, jak bych chtěl .

Jsem v mírném pokušení unést zkratku „OO“ tak, aby znamenala „pouze objekty“ místo „objektově orientovaný“, ale vsadím se, že ani to by pravděpodobně nikam nevedlo. Pro naše účely tedy řekněme, žeJavaScript je „objektově založený (OB)“ objasnit proti „objektově orientovanému (OO)“.

Ať už to nazýváme jakkoli, normálně se do tohoto mechanismu objektu napojíme tak, že postupujeme „OO způsobem“:vytvoříme funkci, kterou použijeme jako „konstruktor“, a tuto funkci nazýváme new abychom mohli „instanciovat“ naši „třídu“, kterou určíme funkcí konstruktoru spolu s jejím následným .prototype dodatky... ale to všechno je jako kouzelnický trik, který vás oslní tady odvést vaši pozornost od toho, co se skutečně děje tam .

Na konci triku je opravdu důležité, že dva objekty jsou spolu vzájemně propojeny pomocí [[Prototype]] řetězu .

Codez Plz

Než budeme moci odvodit a pochopit tento jednodušší pohled na „pouze objekty“ nebo „založené na objektech“, musíme porozumět tomu, co se vlastně vytváří a spojuje, když vytváříme nějaké „zděděné“ objekty v JavaScriptu. Nejen, že uvidíme, co se stane ve výchozím nastavení, ale i to, co ne stane.

Vezměte si tento kód jako náš hlavní příklad:

function Foo(who) {
    this.me = who;
}

Foo.prototype.identify = function() {
    return "I am " + this.me;
};

function Bar(who) {
    Foo.call(this,who);
}

Bar.prototype = Object.create(Foo.prototype);
// NOTE: .constructor is borked here, need to fix

Bar.prototype.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = new Bar("b1");
var b2 = new Bar("b2");

b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."

Poznámka: Někteří lidé píší Bar.prototype = Object.create(Foo.prototype); jako Bar.prototype = new Foo(); . Oba přístupy skončí se stejnými propojenými objekty, kde Bar.prototype je objekt propojený přes jeho[[Prototype]] na Foo.prototype . Jediný skutečný rozdíl je, zda Foo či nikoli funkce je volána během vytváření Bar.prototype . V závislosti na vašich okolnostech a záměru můžete nebo nemusíte chtít, aby se to stalo, takže je považujme za zhruba zaměnitelné, ale s různými účely.

To, co máme, je objekt označený Foo.prototypeidentify() metoda a další objekt nazvanýBar.prototypespeak() metoda. Bar.prototype je nový prázdný objekt to je [[Prototype]] -propojeno s Foo.prototype . Pak máme dva objekty b1b2 , z nichž každý je propojen prostřednictvím vlastního [[Prototype]] na Bar.prototype . b1b2 mít také „vlastněnou nemovitost“ přímo na každé z nich s názvem me , který obsahuje hodnoty "b1" a "b2".

Pojďme se vizuálně podívat na vztahy implikované výše uvedeným fragmentem kódu:

Poznámka: Všechny [[Prototype]] odkazy v diagramu také zmiňují vlastnost „.__proto__“. __proto__ je dříve nestandardní vlastnost (která existuje ve většině, ale ne ve všech prostředích JS), která odhaluje interní [[Prototype]] řetěz. Od ES6 však bude standardizován.

Záměrně jsem z toho diagramu vynechal spoustu detailů, takže to bylo i vzdáleně stravitelné. Ale samozřejmě, protože JS jsou všechny objekty, všechny vazby a původ každé položky lze plně vysledovat. Ke všem vynechaným částem tohoto diagramu se za chvíli vrátíme.

Všimněte si v tomto diagramu, že všechny konstruktory funkcí mají .prototype vlastnost ukazující na objekt. Jak jsme navrhovali, objekt je to, na čem nám opravdu záleží, a při tomto způsobu zobrazení mechanismu objektů JS tento objekt získáme tak, že se podíváme na .prototype funkce konstruktoru. . Funkce ve skutečnosti neplní žádnou zvlášť důležitou roli.

Vím, že spousta z vás právě křičela:"Jistě, že ano! spustí kód konstruktoru pro inicializaci nového objektu!" Dobře, technicky máš pravdu. Foo() má v sobě nějaký kód, který je nakonec spuštěn proti b1b2 .

Ale ďábel je vždy v detailech. Za prvé, nepotřebujeme funkci konstruktoru ke spuštění takového kódu. To je jen jeden způsob, jak dosáhnout takového výsledku. A navrhnu, že je to více rušivý přístup.

Za druhé, na rozdíl od C++ je základní třída/nadtřída Foo() „constructor“ se při spuštění podřízené třídy Bar() nezavolá automaticky "konstruktor", aby se b1b2 . Takže, stejně jako Java, musíme ručně zavolat Foo() funkce z Bar() , ale na rozdíl od Java to musíme udělat s obměnou explicitního vzoru „mixin“ (zde bych to pravděpodobně nazval „implicitní mixin“), aby to fungovalo tak, jak očekáváme. To je ošklivý detail, který lze velmi snadno zapomenout nebo se pomýlit.

Takže tam, kde byste se mnou pravděpodobně namítali, že funkce „konstruktor“ jsou užitečné, když se automaticky volají při konstrukci objektu, bych upozornil, že to platí pouze pro okamžitou úroveň, nikoli pro celý „řetězec dědičnosti“. ", což znamená, že automatické chování je z hlediska užitečnosti dost omezené/mělké.

Redukce polymorfismu

Navíc zde vidíme první náznak problémů s relativním polymorfismem v JS: to nemůžete! Nemohu říctBar() automaticky a relativně volat konstruktor(y) svého předka prostřednictvím relativní reference. Musím ručně zavolat (také „vypůjčit“) Foo() funkce (zde to není konstruktor, jen normální volání funkce!) zevnitř Bar() a ujistěte se, že this je svázán správně, musím udělat trochu nešikovnější .call(this) styl kódu. Fuj.

Co nemusí být zřejmé, dokud se nevrátíte a nepodíváte se blíže na výše uvedený diagram, je, že Foo() funkcenení související jakýmkoli užitečným/praktickým způsobem s Bar() funkce. Foo() funkce se ani neobjevuje v řetězci „dědění“ (neboli „delegace“) Bar.prototype objekt. Skutečnost, že v grafu můžete sledovat některé čáry pro nepřímé vztahy, neznamená, že tyto vztahy jsou tím, na co byste se ve svém kódu chtěli spolehnout.

Problém s polymorfismem, který zde vidíme, se netýká pouze „konstruktorských“ funkcí. Jakákoli funkce na jedné úrovni [[Prototype]] řetězec, který chce zavolat předchůdce se stejným jménem, ​​tak musí učinit pomocí tohoto ručního implicitního mixinového přístupu, stejně jako jsme to udělali v Bar() výše. Nemáme žádný účinný způsob, jak vytvářet relativní reference v řetězci.

Důležité je, že to znamená, že nejen vytvoříme spojení mezi BarFoo jednou v definici "třídy", ale každý jednotlivý polymorfní odkaz musí být také pevně zakódován s přímým vztahem. To výrazně snižuje flexibilitu a udržovatelnost vašeho kódu. Jakmile vytvoříte funkci pevně zakódovanou s implicitním mixinem do „předka“, vaši funkci si nyní nemohou tak snadno „vypůjčit“ jiné objekty bez těchto možných nezamýšlených vedlejších efektů.

Dobře, takže řekněme, že se mnou v tomto bodě souhlasíte, že polymofismus v JS je větší problém, než stojí za to. Použití kódování založeného na konstruktoru ke vzájemnému propojení objektů JS vás nutí k problémům polymorfismu .

.konstruktor

Dalším detailem, který snadno přehlédnete, je .constructor objektu nemovitost se opravdu nechová tak, jak bychom pravděpodobně očekávali. Je to správně na Foo() úrovni grafu, ale níže, na Bar()b1b2 , všimněte si, že zde předpokládané propojení ukazuje .constructor odkazy kupodivu stále ukazují na Foo .

Ve skutečnosti to znamená, že jediný čas je .constructor vlastnost je přidána k objektu, když je tento objekt výchozí .prototype připojené k deklarované funkci, jako je tomu v případě Foo() . Když jsou objekty vytvořeny pomocí new Fn() nebo Object.create(..) hovory, tyto objekty nedělají získat .constructor přidáno k nim.

Řeknu to znovu:objekt vytvořený konstruktorem ve skutečnosti nezíská .constructor vlastnost, která ukazuje, kterým konstruktorem byl vytvořen. Toto je extrémně běžné mylná představa.

Pokud tedy odkazujete na b1.constructor pak například ve skutečnosti delegujete několik článků v řetězci na Foo.prototype . Samozřejmě Foo.prototype má .constructor vlastnost a ukazuje na Foo jak byste očekávali.

Co to znamená? Ve výše uvedeném úryvku ihned po provedení Bar.prototype = Object.create(Foo) (nebo i když jste provedli Bar.prototype = new Foo() ), pokud se plánujete spolehnout na .constructor vlastnost (což mnozí dělají), musíte provést další krok přímo tam, kam jsem vložil komentář „Poznámka:“ JS:

//...
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.constructor = Bar; // <-- add this line!
//...

Poté b1.constructor odkazy budou delegovány na toto Bar.prototype úroveň a bude „správně“ ukazovat na Bar() jak byste pravděpodobně očekávali. Uf...**další syntaxové chyby**, které za nás musí knihovny uživatelského prostředí vždy „opravit“.

Kromě toho skutečnost, že Foo.prototype má .constructor vlastnost ukazující na Foo je zvláštní, když přemýšlíte o "konstruktérovi" tak, jak to dělá většina lidí. Je hezké, že poskytuje objekty vytvořené pomocí new Foo() způsob delegování na .constructor přístup k nemovitosti a vyhledejte Foo() , ale je to bizarní, kde .constructor skutečně žije.

Znamená to, že Foo() vytvořeno Foo.prototype , ale to je nesmysl. Foo() nemělo nic společného s vytvořením výchozího Foo.prototype . Foo.prototype výchozí je prázdný objekt, který byl ve skutečnosti vytvořen vestavěným Object() konstruktoru .

Musíme tedy změnit způsob, jakým přemýšlíme o .constructor majetkové prostředky. Není to ne znamená "konstruktor, kterým byl tento objekt vytvořen". Je to ve skutečnosti znamená „konstruktor, který vytváří všechny objekty, které nakonec získají [[Prototype]] spojený s tímto objektem." Jemný, ale mimořádně důležitý rozdíl, abychom se dostali přímo.

Směřovat? K těmto nejasnostem dochází/záleží pouze v případě, že používáte kód ve stylu konstruktoru, takže je volba tohoto stylu kódu který vás zapojí do problémů. Nemáte nemáte žít s tou bolestí. Existuje lepší a jednodušší způsob!

Celý koláč

Nyní se podívejme na vše, co ve skutečnosti znamená výše uvedený fragment kódu. Jste připraveni na celou tu špinavou věc?

Udělejte si pár minut, abyste to všechno vzali dovnitř. Proč vám ukazovat tak složitý diagram?

Tento diagram vám ve skutečnosti ukazuje, odkud se berou některé funkce JavaScriptu, kde jste předtím možná nikdy neuvažovali o tom, jak to všechno funguje. Zajímalo vás například, jak mohou všechny funkce využívat chování, jako je call()apply()bind() , atd? Možná jste předpokládali, že každá funkce má toto chování zabudované, ale jak můžete vidět z tohoto diagramu, funkce delegují zvýšit jejich [[Prototype]] řetězec, který toto chování zvládne.

Přestože je část delegování chování rozumná a užitečná, zvažte veškerou předpokládanou složitost kódování ve stylu konstruktoru, jak je zde vizualizováno. Je docela těžké vysledovat všechny různé entity a diagramy a dát tomu všemu smysl. Velká část této složitosti pochází z konstruktorů funkcí. (zde je stejný úplný graf, ale s vynechanými čárami implikovaného vztahu, pokud to pomůže strávit)

Pokud vezmete tento diagram a odstraníte všechny funkce a všechny související šipky (což za chvíli uvidíme), zbyde vám „pouze objekty“ a budete mít hodně zjednodušený pohled na svět objektů JS.

Jednodušší:Objekt -> Objekt

Pro osvěžení stejný kód ve stylu prototypu výše:

function Foo(who) {
    this.me = who;
}

Foo.prototype.identify = function() {
    return "I am " + this.me;
};

function Bar(who) {
    Foo.call(this,who);
}

Bar.prototype = Object.create(Foo.prototype);
// NOTE: .constructor is borked here, need to fix

Bar.prototype.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = new Bar("b1");
var b2 = new Bar("b2");

b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."

Podívejme se nyní místo toho na tento alternativní úryvek kódu, který dosahuje přesně toho samého, ale dělá to bez jakéhokoli zmatku nebo rozptýlení „funkcí konstruktoru“, new.prototype , atd. Pouze vytvoří několik objektů a spojí je dohromady.

var Foo = {
    init: function(who) {
        this.me = who;
    },
    identify: function() {
        return "I am " + this.me;
    }
};

var Bar = Object.create(Foo);

Bar.speak = function() {
    alert("Hello, " + this.identify() + ".");
};

var b1 = Object.create(Bar);
b1.init("b1");
var b2 = Object.create(Bar);
b2.init("b2");

b1.speak(); // alerts: "Hello, I am b1."
b2.speak(); // alerts: "Hello, I am b2."

Zkusme porovnat pohledy mezi tímto úryvkem a předchozím. Oba dosahují stejné věci, ale jsou zde některé důležité rozdíly v tom, jak se tam dostaneme.

Nejprve BarFoo jsou nyní jen objekty , už to nejsou funkce ani konstruktory. Nechal jsem je jako velká písmena jen kvůli symetrii a protože se s nimi někteří lidé cítí lépe. Dávají jasně najevo, že propojované objekty jsou tím, na čem nám celou dobu záleželo, takže místo nepřímosti propojení Bar.prototype na Foo.prototype , děláme jen FooBar samotné objekty a je propojit . A , potřebujeme k jejich propojení pouze jeden řádek kódu namísto extra ošklivého polymorfního propojení. Bam!

Místo volání konstruktorů funkcí jako new Bar(..) , používáme Object.create(..) , což je pomocník ES5, který nám umožňuje vytvořit nový objekt a volitelně poskytnout další objekt [[Prototype]] propojit to. Dostaneme stejný výsledek (vytvoření objektu a propojení) jako volání konstruktoru, ale aniž bychom konstruktor potřebovali. BTW, existuje jednoduchá polyfill mimo ES5 pro Object.create(..) , takže tento styl kódu můžete bez obav používat ve všech prohlížečích.

Za druhé, povšimněte si, že protože se už nestaráme o konstruktory, eliminovali jsme jakékoli obavy z nepohodlných polymorfismů, které nás nutí provádět manuální implikované mixy pro volání Foo()Bar() . Místo toho jsme vložili kód, který jsme chtěli spustit pro inicializaci našich objektů, do init() metodou na Foo a nyní můžeme volat b1.init(..) přímo přes řetězec delegování a „magicky“ to funguje tak, jak chceme.

Takže tady máme kompromis. Nedostáváme automatická volání konstruktoru, což znamená, že vytváříme objekt jako var b1 = Object.create(Bar) a pak musíme dodatečně zavolat b1.init("b1") . To je "další kód".

Ale výhody, které získáváme, které jsou podle mě mnohem lepší a stojí za to , nejsou žádné trapnosti s propojením mezi FooBar -- místo toho využíváme [[Prototype]] delegování, abyste dosáhli opětovného použití kódu vinit() . Už žádné upovídané/opakující se .prototype odkazy a ani my nemusíme používat .call(this) téměř stejně často (zejména pokud se vyhneme polymorfismus!).

Vzhled je všechno

A abychom si představili jednoduchost, kterou nám tento přístup přináší, zde je diagram, kdy zcela odstraníme funkce a zaměříme se pouze na objekty:

Nevím jak vy, ale já si prostě myslím, že mentální model je tak čistší a bonusem je, že jeho sémantika dokonale odpovídá kódu.

Ukázal jsem vám dostatečně jednoduchý kód používající pouze základní syntaxi JS, že nepotřebuji žádné pomocné knihovny k propojení mých objektů. Samozřejmě, že bych mohl použít jeden, ale proč? Jednodušší je lepší. KISS.

Pro záznam, nejsem ani na dálku tady génius. Brendan Eich, tvůrce našeho jazyka, byl génius pro vytvoření něčeho tak mocného a přitom tak jednoduchého.

Sebereflexe objektu

Poslední věc, kterou je třeba řešit:jak toto zjednodušení ovlivňuje proces uvažování o předmětu? Jinými slovy, můžeme si objekt prohlédnout a zjistit jeho vztahy k jiným objektům?

U kódu ve stylu prototypu vypadá odraz takto:

b1 instanceof Bar; // true
b2 instanceof Bar; // true
b1 instanceof Foo; // true
b2 instanceof Foo; // true
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf(b1) === Bar.prototype; // true
Object.getPrototypeOf(b2) === Bar.prototype; // true
Object.getPrototypeOf(Bar.prototype) === Foo.prototype; // true

Všimněte si, že používáte instanceof a musíte přemýšlet z hlediska funkcí konstruktoru, které vytvořily vaše objekty, a jejich .prototype s, spíše než jen reflektovat objekty samotné. Každá z těchto úvah má ve výsledku o něco větší mentální daň.

A když existují pouze předměty?

Bar.isPrototypeOf(b1); // true
Bar.isPrototypeOf(b2); // true
Foo.isPrototypeOf(b1); // true
Foo.isPrototypeOf(b2); // true
Foo.isPrototypeOf(Bar); // true
Object.getPrototypeOf(b1) === Bar; // true
Object.getPrototypeOf(b2) === Bar; // true
Object.getPrototypeOf(Bar) === Foo; // true

Naproti tomu reflexe na předmětech je pouze o předmětech. Neexistují žádné nepříjemné odkazy na .prototype konstruktoru majetek pro kontroly. Pomocí [[Prototype]] můžete zkontrolovat, zda spolu jeden objekt souvisí na jiný objekt. Stejné schopnosti jako výše, ale s menší duševní daní.

Kromě toho, jak jsem zmínil v části 2, tento druh explicitního odrazu objektu je vhodnější a je robustnější/spolehlivější než implicitní detekce pomocí typu duck.

Object.wrapItUpAlready()

Zhluboka se nadechnout! Bylo toho hodně. Pokud jste sledovali všechny 3 díly série článků, doufám, že už vidíte závěr:JS má objekty a když je propojíme, získáme mocné delegování chování.

Prostě není potřeba hromadit třídní orientaci na vrcholu tak skvělého systému, protože to nakonec vede jen ke zmatku a rozptýlení, které udrželo objektový mechanismus JS zahalený a zakrytý všemi těmito pomocnými knihovnami a nedorozuměními o syntaxi JS.

Pokud přestanete přemýšlet o dědictví a místo toho budete myslet se šipkami namířenými opačným směrem:delegování, váš kód JS bude jednodušší. Pamatujte:jsou to pouze objekty spojené s objekty!