Een handleiding voor op prototypen gebaseerde klassenovererving in JavaScript

Computertalen bieden vaak een manier om van een object te erven
een ander object. Het overgenomen object bevat alle eigenschappen van het bovenliggende object. Bovendien zal het ook zijn eigen set unieke eigenschappen specificeren.

Volg mij op Twitter voor JavaScript-tips en boekaankondigingen.

JavaScript-objecten gebruiken op prototype gebaseerde overerving. Het ontwerp is logisch vergelijkbaar (maar verschilt in implementatie) van klassevererving in strikt objectgeoriënteerde programmeertalen.

Het kan losjes worden beschreven door te zeggen dat wanneer methoden of eigenschappen zijn gekoppeld aan het prototype van het object, deze beschikbaar komen voor gebruik op dat object en zijn nakomelingen. Maar dit proces speelt zich vaak achter de schermen af.

Wanneer u code schrijft, hoeft u de eigenschap prototype niet eens rechtstreeks aan te raken. Wanneer u de split-methode uitvoert, zou u deze rechtstreeks vanuit een letterlijke tekenreeks aanroepen als: "hallo" .split ("e") of vanuit een variabele: string.split (",");

Wanneer u klasse gebruikt en trefwoorden intern uitbreidt, gebruikt JavaScript nog steeds op prototypen gebaseerde overerving. Het vereenvoudigt alleen de syntaxis. Misschien is dit de reden waarom het belangrijk is om te begrijpen hoe prototype-overerving werkt. Het is nog steeds de kern van het taalontwerp.

Daarom zie je in veel tutorials String.prototype.split geschreven in plaats van alleen String.split. Dit betekent dat er een methodesplitsing is die kan worden gebruikt met objecten van het type string omdat deze is gekoppeld aan de prototype-eigenschap van dat object.

Een logische hiërarchie van objecttypen maken

Kat en hond zijn geërfd van huisdier dat is geërfd van dier.

Een hond en een kat hebben vergelijkbare eigenschappen. In plaats van twee verschillende klassen te maken,
we kunnen eenvoudig één klasse huisdier maken en er kat en hond van erven. Maar de Pet-klasse zelf kan ook worden geërfd van de klasse Animal.

Voor we beginnen

Proberen om prototypes te begrijpen is als het oversteken van de rivier, van codering tot computertaalontwerp. Twee totaal verschillende kennisgebieden.

Technisch gezien is alleen de lichte kennis van de klas en het uitbreiden van trefwoorden voldoende om software te schrijven. Proberen het prototype te begrijpen, is alsof je je in de donkere hoeken van het taalontwerp begeeft. En soms kan dat inzichtelijk zijn.

Deze tutorial alleen zal niet genoeg zijn. Ik heb me alleen gericht op enkele belangrijke dingen die je hopelijk in de goede richting zullen leiden.

Onder de motorkap

Het idee achter object-overerving is om structuur te bieden voor een hiërarchie van
soortgelijke objecten. Je kunt ook zeggen dat een onderliggend object is "afgeleid" van zijn ouder.

Hoe prototypeketens worden gemaakt in JavaScript.

Technisch gezien is dit hoe het eruit ziet. Probeer hier niet teveel aan te denken. Weet gewoon dat helemaal bovenaan de hiërarchie het object Object staat. Daarom verwijst zijn eigen prototype naar nul. Er zit niets anders boven.

Op prototype gebaseerde Object Inheritance

JavaScript ondersteunt object-overerving via zoiets als prototypes. Er is een objecteigenschap genaamd prototype aan elk object gekoppeld.

Werken met de klas en het uitbreiden van trefwoorden is eenvoudig, maar het is niet triviaal om te begrijpen hoe prototype-gebaseerde overerving werkt. Hopelijk zal deze tutorial op zijn minst een deel van de mist wegnemen!

Object Constructor Functies

Functies kunnen worden gebruikt als objectconstructors. De naam van een constructorfunctie begint meestal met een hoofdletter om het onderscheid tussen reguliere functies te maken. Objectconstructors worden gebruikt om een ​​instantie van een object te maken.

Sommige van de ingebouwde JavaScript-objecten zijn al gemaakt volgens dezelfde regels. Number, Array en String worden bijvoorbeeld overgenomen van Object. Zoals we eerder hebben besproken, betekent dit dat elke eigenschap die is gekoppeld aan Object automatisch beschikbaar wordt voor al zijn onderliggende items.

constructors

Het is onmogelijk om een ​​prototype te begrijpen zonder de anatomie van constructorfuncties te begrijpen.

Wat gebeurt er precies wanneer we een aangepaste constructorfunctie maken? Twee eigenschappen verschijnen op magische wijze in onze klassedefinitie: constructor en prototype.constructor.

Ze wijzen niet op hetzelfde object. Laten we ze opsplitsen:

Stel dat we een nieuwe klasse Crane definiëren (met behulp van een functie of een klasse-trefwoord).

Een aangepaste constructor die we zojuist hebben gemaakt, is nu gekoppeld aan de prototype-eigenschap van onze aangepaste Crane-klasse. Het is een link die verwijst naar zijn eigen constructor. Het creëert circulaire logica. Maar dat is maar een stukje van de puzzel.

Laten we nu eens kijken naar Crane.constructor:

Crane.constructor zelf verwijst naar het type object waaruit het is gemaakt.
Omdat alle objectconstructors native functies zijn, verwijst het object Crane.constructor naar een object van het type Functie, met andere woorden de functieconstructor.

Deze dynamiek tussen Crane.prototype.constructor en Crane.constructor maakt prototype-overerving op moleculair niveau mogelijk. Bij het schrijven van JavaScript-code hoeft u hier zelden over na te denken. Maar dit is absoluut een interviewvraag.

Laten we dit nog eens kort bespreken. Crane.prototype.constructor wijst naar zijn eigen constructor. Het is bijna alsof je zegt: "Ik ben mij".

Hetzelfde gebeurt precies wanneer u een klasse definieert met behulp van een klassezoekwoord:

Maar de eigenschap Crane.constructor verwijst naar de functie Constructor.

En zo wordt de link gelegd.

Nu kan het Crane-object zelf het 'prototype' van een ander object zijn. En dat object kan het prototype zijn van een ander object. Enzovoort. De ketting kan eeuwig doorgaan.

Kant Opmerking: in het geval van de functies in ES5-stijl is de functie zelf de
constructeur. Maar het sleutelwoord van de ES6-klasse plaatst de constructor binnen zijn bereik. Dit is slechts een syntactisch verschil.

Op prototype gebaseerde erfenis

We moeten altijd de klasse gebruiken en trefwoorden uitbreiden om objecten te maken en over te nemen. Maar ze zijn slechts een snoeppapiertje voor wat er achter de schermen gebeurt.

Hoewel het maken van object-overervingshiërarchieën met behulp van de syntaxis van de ES5-stijl al lang verouderd is en zelden wordt gezien bij professionele softwareontwikkelaars, krijgt u door een beter inzicht te krijgen in hoe het eigenlijk werkt.

Laten we een nieuw object Bird definiëren en 3 eigenschappen toevoegen: type, kleur en eieren. Laten we ook 3 methoden toevoegen: vliegen, lopen en lay_egg. Iets dat alle vogels kunnen doen:

Merk op dat ik opzettelijk de lay_egg-methode grijs heb gemaakt. Onthoud hoe wij
eerder besproken dat Bird.prototype verwijst naar zijn eigen constructor?

Je zou de lay egg-methode ook rechtstreeks aan Bird.prototype kunnen hebben gekoppeld, zoals in het volgende voorbeeld:

Op het eerste gezicht lijkt het misschien alsof er geen verschil is tussen het koppelen van methoden met het trefwoord this in Bird en het eenvoudigweg toevoegen van de eigenschap Bird.prototype. Omdat het nog steeds goed werkt?

Maar dit is niet helemaal waar. Ik ga nog niet in op de details, omdat ik het onderscheid hier niet volledig begrijp. Maar ik ben van plan deze zelfstudie bij te werken wanneer ik wat meer inzicht in het onderwerp verzamel.

(opmerkingen van prototype veteranen zijn welkom!)

Niet alle vogels zijn hetzelfde

Het hele punt van objectovererving is het gebruik van één gemeenschappelijke klasse die alle eigenschappen en methoden definieert die alle kinderen van die klasse automatisch zullen erven. Dit maakt code korter en bespaart geheugen.

(Stel je voor dat je dezelfde eigenschappen en methoden voor alle onderliggende objecten opnieuw definieert. Het zou twee keer zoveel geheugen kosten.)

Laten we verschillende soorten vogels maken. Hoewel ze allemaal nog kunnen vliegen, lopen en lay_eggs (omdat ze zijn geërfd van de hoofdklasse Bird), voegt elk uniek vogelsoort zijn eigen methoden toe die uniek zijn voor die klasse. Alleen papegaaien kunnen bijvoorbeeld praten. En alleen raven kunnen puzzels oplossen. Alleen een zangvogel kan zingen.

Papegaai
Laten we een papegaai maken en overnemen van Bird:

Parrot is een normale constructorfunctie net als Bird.

Het verschil is dat we Bird's constructor met Bird.call noemen en deze context doorgeven aan de Parrot, voordat we onze eigen methoden koppelen. Bird.call voegt eenvoudigweg alle eigenschappen en methoden toe aan Parrot. Daarnaast voegen we ook onze eigen methode toe: praten.

Nu kunnen papegaaien vliegen, lopen, eieren leggen en praten! Maar we hoefden nooit fly walk en lay_eggs-methoden in Parrot zelf te definiëren.

Raaf
Laten we op dezelfde manier Raven maken en overnemen van Bird:

Raven zijn uniek omdat ze puzzels kunnen oplossen.

Zangvogel
Laten we nu Songbird maken en overnemen van Bird:

Zangvogels kunnen zingen.

De vogels testen

We hebben zojuist een stel verschillende vogels gemaakt met unieke vaardigheden. Laten we eens kijken wat
ze zijn in staat! Tot nu toe hebben we alleen klassen gedefinieerd en hun vastgesteld
hiërarchische relatie.

Om met objecten te werken, moeten we ze instantiëren:

Laten we een mus instantiëren met de originele Bird-constructor:

Mus kan vliegen, lopen en eieren leggen, omdat het is geërfd van Bird dat al die methoden definieert.

Maar een mus kan niet praten. Omdat het geen papegaai is.

Laten we een parkiet maken uit de klasse Parrot:

Omdat Parrot is geërfd van Bird, krijgen we alle methoden. Een parkiet heeft het unieke vermogen om te praten, maar hij kan niet zingen! De methode zingen is alleen beschikbaar op objecten van het type Songbird. Laten we de spreeuw uit de Songbird-klasse overnemen:

Laten we tot slot een raaf maken en enkele puzzels oplossen:

Klasse gebruiken en zoekwoorden uitbreiden

De constructeurs in ES5-stijl kunnen een beetje omslachtig zijn.

Gelukkig hebben we nu klasse en breiden we trefwoorden uit om precies hetzelfde te bereiken als we in de vorige sectie hebben gedaan.

klasse vervangt functie

verlengt en super () vervang Bird.call van de vorige voorbeelden.

Merk op dat we super () moeten gebruiken die de constructor van de bovenliggende klasse aanroept.

Deze syntaxis ziet er veel beter beheersbaar uit!

Nu hoeven we alleen nog maar het object te instantiëren:

Overzicht

Klasseovererving helpt bij het vaststellen van een hiërarchie van objecten.

Klassen zijn de fundamentele bouwstenen van uw applicatieontwerp en -architectuur. Ze maken het werken met code een beetje menselijker.

Natuurlijk was Bird slechts een voorbeeld. In een real-world scenario kan het van alles zijn op basis van het type applicatie dat u probeert te bouwen.

Voertuigklasse kan een ouder van motorfiets, auto of tank zijn.

Vis kan worden gebruikt om haaien, goudvissen, snoeken enzovoort te erven.

Overerving helpt ons schonere code te schrijven en het bovenliggende object opnieuw te gebruiken om geheugen te besparen bij het herhalen van objecteigenschappen en methodedefinities.