Výhody prototypové dědičnosti oproti klasické?

Tak jsem se konečně po těch letech přestal tahat a rozhodl se naučit JavaScript „pořádně“. Jedním z nejvíce drásajících prvků návrhu jazyků je implementace dědičnosti. Díky zkušenostem s Ruby jsem byl opravdu rád, že jsem viděl uzávěry a dynamické psaní; ale za život nemůžu přijít na to, jaké výhody plynou z instancí objektů využívajících jiné instance pro dědění.

Odpověď

Vím, že tato odpověď je o 3 roky později, ale opravdu si myslím, že současné odpovědi neposkytují dostatek informací o tom, jak je prototypová dědičnost lepší než klasická dědičnost.

Nejprve se podívejme na nejčastější argumenty, které programátoři JavaScriptu uvádějí na obranu prototypové dědičnosti (tyto argumenty přebírám z aktuálního souboru odpovědí):

  1. Je to jednoduché.
  2. Je to mocné.
  3. To vede k menšímu, méně nadbytečnému kódu.
  4. Je dynamický, a proto je lepší pro dynamické jazyky.

Nyní jsou všechny tyto argumenty platné, ale nikdo se neobtěžoval vysvětlit proč. Je to jako říkat dítěti, že studium matematiky je důležité. Jistě, ale dítěti to rozhodně nevadí; a nemůžete udělat dítě jako matematiku tím, že řeknete, že je to důležité.

Myslím, že problém s prototypovou dědičností je v tom, že je vysvětlena z pohledu JavaScriptu. Mám rád JavaScript, ale prototypová dědičnost v JavaScriptu je špatná. Na rozdíl od klasické dědičnosti existují dva vzory prototypové dědičnosti:

  1. Prototypový vzor prototypové dědičnosti.
  2. Vzor konstruktoru prototypové dědičnosti.

JavaScript bohužel používá konstruktorový vzor prototypové dědičnosti. Je to proto, že když byl vytvořen JavaScript, Brendan Eich (tvůrce JS) chtěl, aby vypadal jako Java (která má klasickou dědičnost):

A prosazovali jsme to jako malého bratra Javy, jako komplementární jazyk, jako byl Visual Basic k C++ v jazykových rodinách Microsoftu v té době.

To je špatné, protože když lidé používají konstruktory v JavaScriptu, myslí si konstruktory dědící od jiných konstruktorů. To je špatně. V prototypové dědičnosti objekty dědí z jiných objektů. Konstruktéři nikdy nevstupují do obrazu. To je to, co většinu lidí mate.

Lidé z jazyků, jako je Java, která má klasickou dědičnost, jsou ještě zmatenější, protože ačkoli konstruktory vypadají jako třídy, nechovají se jako třídy. Jak uvedl Douglas Crockford:

Toto nasměrování bylo zamýšleno tak, aby se jazyk klasicky vyškoleným programátorům zdál známější, ale nepodařilo se to, jak můžeme vidět z velmi nízkého názoru programátorů Java na JavaScript. Vzor konstruktoru JavaScriptu neoslovil klasický dav. Také to zakrylo skutečnou prototypovou povahu JavaScriptu. Výsledkem je, že existuje velmi málo programátorů, kteří vědí, jak jazyk efektivně používat.

Tady to máš. Přímo z tlamy koně.

Opravdová prototypová dědičnost

Prototypová dědičnost je o objektech. Objekty dědí vlastnosti z jiných objektů. To je vše. Existují dva způsoby vytváření objektů pomocí prototypové dědičnosti:

  1. Vytvořte zcela nový objekt.
  2. Klonujte existující objekt a rozšiřte jej.

Poznámka: JavaScript nabízí dva způsoby klonování objektu – delegování a zřetězení. Od nynějška budu slovo „klon“ používat výhradně k odkazování na dědění prostřednictvím delegování a slovo „kopírovat“ k výhradnímu odkazování na dědění prostřednictvím zřetězení.

Dost řečí. Podívejme se na několik příkladů. Řekněme, že mám kruh o poloměru 5 :

var circle = {
    radius: 5
};

Plochu a obvod kruhu můžeme vypočítat z jeho poloměru:

circle.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

circle.circumference = function () {
    return 2 * Math.PI * this.radius;
};

Nyní chci vytvořit další kruh o poloměru 10 . Jedním ze způsobů, jak to udělat, by bylo:

var circle2 = {
    radius: 10,
    area: circle.area,
    circumference: circle.circumference
};

JavaScript však poskytuje lepší způsob – delegování. Object.create k tomu slouží funkce:

var circle2 = Object.create(circle);
circle2.radius = 10;

To je vše. Právě jste provedli prototypovou dědičnost v JavaScriptu. nebylo to jednoduché? Vezmete předmět, naklonujete ho, změníte, co potřebujete, a hej, máte zbrusu nový předmět.

Nyní se můžete ptát:„Jak je to jednoduché? Pokaždé, když chci vytvořit nový kruh, musím naklonovat circle a ručně mu přiřadit rádius“. Řešením je použít funkci, která to těžké zvedne za vás:

function createCircle(radius) {
    var newCircle = Object.create(circle);
    newCircle.radius = radius;
    return newCircle;
}

var circle2 = createCircle(10);

Ve skutečnosti můžete toto vše zkombinovat do jediného objektového literálu následovně:

var circle = {
    radius: 5,
    create: function (radius) {
        var circle = Object.create(this);
        circle.radius = radius;
        return circle;
    },
    area: function () {
        var radius = this.radius;
        return Math.PI * radius * radius;
    },
    circumference: function () {
        return 2 * Math.PI * this.radius;
    }
};

var circle2 = circle.create(10);

Prototypální dědičnost v JavaScriptu

Pokud si ve výše uvedeném programu všimnete create vytvoří klon circle , přiřadí nový radius k němu a poté jej vrátí. Přesně to dělá konstruktor v JavaScriptu:

function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.area = function () {
    var radius = this.radius;
    return Math.PI * radius * radius;
};

Circle.prototype.circumference = function () {         
    return 2 * Math.PI * this.radius;
};

var circle = new Circle(5);
var circle2 = new Circle(10);

Vzor konstruktoru v JavaScriptu je obrácený prototypový vzor. Místo vytvoření objektu vytvoříte konstruktor. new klíčové slovo váže this ukazatel uvnitř konstruktoru na klon prototype konstruktoru.

Zní to zmateně? Je to proto, že konstruktorový vzor v JavaScriptu věci zbytečně komplikuje. To je pro většinu programátorů obtížné pochopit.

Místo toho, aby přemýšleli o objektech dědících od jiných objektů, myslí na konstruktory dědící od jiných konstruktérů, a pak jsou naprosto zmatení.

Existuje celá řada dalších důvodů, proč byste se měli vyhnout vzoru konstruktoru v JavaScriptu. Můžete si o nich přečíst v mém příspěvku na blogu zde:Konstruktoři vs Prototypy

Jaké jsou tedy výhody prototypové dědičnosti oproti klasické dědičnosti? Pojďme si znovu projít nejčastější argumenty a vysvětlit proč .

1. Prototypální dědičnost je jednoduchá

CMS ve své odpovědi uvádí:

Podle mého názoru je hlavní výhodou prototypové dědičnosti její jednoduchost.

Zvažme, co jsme právě udělali. Vytvořili jsme objekt circle který měl poloměr 5 . Potom jsme to naklonovali a dali klonu poloměr 10 .

Proto potřebujeme pouze dvě věci, aby prototypová dědičnost fungovala:

  1. Způsob vytvoření nového objektu (např. objektové literály).
  2. Způsob, jak rozšířit existující objekt (např. Object.create ).

Naproti tomu klasická dědičnost je mnohem složitější. V klasickém dědění máte:

  1. Třídy.
  2. Objekt.
  3. Rozhraní.
  4. Abstraktní třídy.
  5. Závěrečné kurzy.
  6. Virtuální základní třídy.
  7. Konstruktéři.
  8. Destruktory.

Dostanete nápad. Jde o to, že prototypová dědičnost je snazší pochopit, snáze implementovat a snáze o ní uvažovat.

Jak to říká Steve Yegge ve svém klasickém blogovém příspěvku „Portrait of a N00b“:

Metadata jsou jakýkoli druh popisu nebo modelu něčeho jiného. Komentáře ve vašem kódu jsou pouze popisem výpočtu v přirozeném jazyce. To, co dělá metadata metadaty, je to, že nejsou nezbytně nutná. Pokud mám psa s nějakými doklady o původu a ztratím papíry, mám stále dokonale platného psa.

Ve stejném smyslu jsou třídy pouze metadata. Třídy nejsou pro dědění striktně vyžadovány. Nicméně pro některé lidi (obvykle n00b) je práce s třídami pohodlnější. Dává jim to falešný pocit bezpečí.

No, také víme, že statické typy jsou jen metadata. Jedná se o specializovaný druh komentářů zaměřených na dva druhy čtenářů:programátory a kompilátory. Statické typy vyprávějí příběh o výpočtu, pravděpodobně proto, aby pomohly oběma skupinám čtenářů pochopit záměr programu. Ale statické typy lze za běhu zahodit, protože jsou to nakonec jen stylizované komentáře. Jsou jako papírování s rodokmenem:může to udělat určitý typ nejistých osobností šťastnějšími ohledně jejich psa, ale psovi to rozhodně nevadí.

Jak jsem uvedl dříve, kurzy dávají lidem falešný pocit bezpečí. Například získáte příliš mnoho NullPointerException je v Javě, i když je váš kód dokonale čitelný. Zjistil jsem, že klasická dědičnost obvykle překáží programování, ale možná je to jen Java. Python má úžasný klasický systém dědičnosti.

2. Prototypální dědičnost je výkonná

Většina programátorů, kteří pocházejí z klasického prostředí, tvrdí, že klasická dědičnost je výkonnější než prototypová dědičnost, protože má:

  1. Soukromé proměnné.
  2. Vícenásobná dědičnost.

Toto tvrzení je nepravdivé. Už víme, že JavaScript podporuje soukromé proměnné prostřednictvím uzávěrů, ale co vícenásobná dědičnost? Objekty v JavaScriptu mají pouze jeden prototyp.

Pravdou je, že prototypová dědičnost podporuje dědění z více prototypů. Prototypální dědičnost jednoduše znamená, že jeden objekt dědí z jiného objektu. Ve skutečnosti existují dva způsoby, jak implementovat prototypovou dědičnost:

  1. Delegování nebo rozdílová dědičnost
  2. Klonování nebo konkatenativní dědičnost

Ano JavaScript umožňuje pouze delegování objektů na jeden jiný objekt. Umožňuje však kopírovat vlastnosti libovolného počtu objektů. Například _.extend dělá právě toto.

Mnoho programátorů to samozřejmě nepovažuje za skutečnou dědičnost, protože instanceof a isPrototypeOf říct jinak. To však lze snadno napravit uložením řady prototypů na každý objekt, který dědí z prototypu prostřednictvím zřetězení:

function copyOf(object, prototype) {
    var prototypes = object.prototypes;
    var prototypeOf = Object.isPrototypeOf;
    return prototypes.indexOf(prototype) >= 0 ||
        prototypes.some(prototypeOf, prototype);
}

Prototypální dědičnost je stejně silná jako klasická dědičnost. Ve skutečnosti je mnohem výkonnější než klasická dědičnost, protože v prototypové dědičnosti si můžete ručně vybrat, které vlastnosti zkopírovat a které vlastnosti z různých prototypů vynechat.

V klasickém dědění je nemožné (nebo přinejmenším velmi obtížné) vybrat, které vlastnosti chcete zdědit. K vyřešení diamantového problému používají virtuální základní třídy a rozhraní.

V JavaScriptu však o problému diamantu s největší pravděpodobností nikdy neuslyšíte, protože můžete přesně určit, které vlastnosti chcete zdědit a od kterých prototypů.

3. Prototypová dědičnost je méně redundantní

Tento bod je trochu obtížnější vysvětlit, protože klasická dědičnost nemusí nutně vést k redundantnímu kódu. Ve skutečnosti se ke snížení redundance v kódu používá dědičnost, ať už klasická nebo prototypová.

Jedním argumentem by mohlo být, že většina programovacích jazyků s klasickou dědičností je staticky typována a vyžaduje, aby uživatel explicitně deklaroval typy (na rozdíl od Haskellu, který má implicitní statické typování). To vede k podrobnějšímu kódu.

Java je tímto chováním notoricky známá. Jasně si pamatuji, jak Bob Nystrom ve svém příspěvku na blogu o Pratt Parsers zmínil následující anekdotu:

Musíte milovat úroveň byrokracie Javy „podepište to ve čtyřech vyhotoveních“.

Znovu si myslím, že je to jen proto, že Java je tak na hovno.

Jedním z platných argumentů je, že ne všechny jazyky, které mají klasickou dědičnost, podporují vícenásobnou dědičnost. Znovu mě napadá Java. Ano Java má rozhraní, ale to nestačí. Někdy opravdu potřebujete vícenásobné dědictví.

Protože prototypová dědičnost umožňuje vícenásobnou dědičnost, kód, který vyžaduje vícenásobnou dědičnost, je méně nadbytečný, pokud je napsán pomocí prototypové dědičnosti, spíše než v jazyce, který má klasickou dědičnost, ale žádnou vícenásobnou dědičnost.

4. Prototypální dědičnost je dynamická

Jednou z nejdůležitějších výhod dědičnosti prototypů je, že k prototypům můžete po jejich vytvoření přidávat nové vlastnosti. To vám umožní přidat do prototypu nové metody, které budou automaticky zpřístupněny všem objektům, které na tento prototyp delegují.

To není možné v klasickém dědění, protože jakmile je třída vytvořena, nemůžete ji za běhu upravovat. Toto je pravděpodobně jediná největší výhoda prototypové dědičnosti oproti klasické dědičnosti a měla být na vrcholu. Nicméně to nejlepší si rád nechávám na konec.

Závěr

Prototypová dědičnost je důležitá. Je důležité poučit programátory JavaScriptu o tom, proč opustit konstruktorový vzor prototypové dědičnosti ve prospěch prototypového vzoru prototypové dědičnosti.

Musíme začít správně učit JavaScript a to znamená ukázat novým programátorům, jak psát kód pomocí prototypového vzoru namísto vzoru konstruktoru.

Nejen, že bude jednodušší vysvětlit prototypovou dědičnost pomocí prototypového vzoru, ale také to udělá lepší programátory.

Pokud se vám tato odpověď líbila, měli byste si také přečíst můj blogový příspěvek na téma „Proč na prototypové dědičnosti záleží“. Věřte mi, nebudete zklamáni.