Kuinka ES6-luokat todella toimivat ja kuinka rakentaa oma

ECMAScriptin kuudes painos (tai lyhyt ES6) mullisti kieltä lisäämällä uusia uusia ominaisuuksia, kuten luokat ja luokkaperusteiset perinnöt. Uutta syntaksia on helppo käyttää ymmärtämättä yksityiskohtia ja se tekee suurimmaksi osaksi mitä odotit, mutta jos olet kuin minä, se ei ole aivan tyydyttävä. Kuinka näennäisesti maaginen syntaksi todella toimii konepellin alla? Kuinka se on vuorovaikutuksessa muiden kielen ominaisuuksien kanssa? Onko mahdollista jäljitellä luokkia käyttämättä luokkasyntaksia? Vastaan ​​täällä näihin kysymyksiin perusteettomasti.

Mutta ensin, jotta ymmärrät luokat, sinun on ymmärrettävä, mikä tuli niiden edelle, ja Javascriptin taustalla oleva kohdemalli.

Kohdemalli

Javascript-objektimalli on melko yksinkertainen. Jokainen objekti on vain merkkijonojen ja symbolien kartoitus ominaisuuskuvauksiin. Jokainen ominaisuuskuvaus pitää puolestaan ​​joko getter / setter -paria laskettuja ominaisuuksia varten tai data-arvon tavallisille dataominaisuuksille.

Kun suoritat koodin foo [bar], se muuntaa palkin merkkijonoksi, jos se ei vielä ole merkkijono tai symboli, etsii sitten tätä avainta foo: n ominaisuuksista ja palauttaa vastaavan ominaisuuden arvon (tai kutsuu sen getter-toimintoa sovellettavissa). Kirjaimellisille merkkijononäppäimille, jotka ovat kelvollisia tunnisteita, on lyhennetty syntaksi foo.bar, joka vastaa foo ["bar"]. Toistaiseksi niin yksinkertainen.

Prototyyppinen perintö

Javascriptissa on niin kutsuttu prototyyppinen perintö, joka kuulostaa pelottavalta, mutta on itse asiassa yksinkertaisempi kuin perinteinen luokkaperintö, kun saat sen ripustettavaksi. Jokaisella objektilla voi olla implisiittinen osoitin toiseen objektiin, jota kutsutaan sen prototyypiksi. Kun yrität käyttää ominaisuutta objektilla, jolla ei ole mitään ominaisuutta tällä avaimella, se etsii sen sijaan prototyyppiobjektin avainta ja palauttaa prototyypin ominaisuuden kyseiselle avaimelle, jos sitä on. Jos sitä ei ole prototyypissä, se tarkistaa rekursiivisesti prototyypin prototyypin ja niin edelleen, koko ketjun, kunnes omaisuus löytyy tai esine, jolla ei ole prototyyppiä, saavutetaan.

Jos olet käyttänyt Pythonia aiemmin, ominaisuuksien hakuprosessi on samanlainen. Python-ohjelmassa kutakin ominaisuutta etsitään ensin ilmentymän sanakirjasta. Jos sitä ei ole läsnä siellä, ajonaika tarkistaa luokkasanakirjan, sitten superluokan sanakirjan ja niin edelleen, aina perinnehierarkian ylöspäin. Javascriptissa prosessi on samanlainen paitsi, että tyyppiobjekteja ja ilmentymiobjekteja ei voida erottaa toisistaan ​​- mikä tahansa objekti voi olla minkä tahansa muun objektin prototyyppi. Tietenkin, todellisessa maailmassa, ihmiset käyttävät tätä tosiasiaa harvoin ja järjestävät sen sijaan koodinsa luokan kaltaisiksi hierarkioiksi, koska tällä tavalla on helpompaa hallita, minkä vuoksi Javascript lisäsi ensin luokkasyntaksin.

Sisäiset lähtö- ja saapumisajat

Jos kaikki esine koostuu on ominaisuuksien avainten kartoitus, minne prototyyppi tallennetaan? Vastaus on, että ominaisuuksien lisäksi objekteilla on myös sisäisiä menetelmiä ja sisäisiä aikavälejä, joita käytetään erityisen kielitason semantiikan toteuttamiseen. Sisäisiin lähtö- ja saapumisaikoihin ei pääse suoraan Javascript-koodista, mutta joissakin tapauksissa on olemassa tapoja käyttää niitä epäsuorasti. Objektien prototyyppejä edustaa esimerkiksi [[Prototype]] -väli, joka voidaan lukea ja kirjoittaa käyttämällä vastaavasti Object.getPrototypeOf () ja Object.setPrototypeOf (). Sopimuksen mukaan sisäiset aikavälit ja menetelmät kirjoitetaan [[kaksoishakasulkeissa]] erottaaksesi ne tavallisista ominaisuuksista.

Vanhan tyylin luokat

Javascriptin varhaisissa versioissa oli yleistä simuloida luokkia käyttämällä seuraavanlaista koodia.

Mistä tämä tuli? Mistä prototyyppi tuli? Mitä uusi tekee? Kuten osoittautuu, edes Javascriptin varhaisimmat versiot eivät halunneet olla liian epätavanomaisia, joten niihin sisältyi syntaksi, jonka avulla voit koodata asioita, jotka olivat kinda-sorta-luokan kaltaisia.

Javascriptin toiminnot määritellään teknisesti kahdella sisäisellä menetelmällä [[Call]] ja [[Construct]]. Kaikkia objekteja, joilla on [[Call]] -menetelmä, kutsutaan funktioksi, ja mitä tahansa toimintoa, jolla on lisäksi [[Construct]] -menetelmä, kutsutaan rakentajaksi1. [[Soita]] -menetelmä määrittää, mitä tapahtuu, kun kutsut objektia funktiona, esim. foo (args), kun taas [[Construct]] määrittelee, mitä tapahtuu, kun kutsut sitä uuteen lausekkeeseen, ts. uudeksi fooksi tai uudeksi fooksi (args).

Tavallisille funktiomääritelmille² kutsumalla [[Construct]] luodaan epäsuorasti uusi objekti, jonka [[Prototyyppi]] on rakentajatoiminnon prototyyppinen ominaisuus, jos ominaisuus on olemassa ja sitä arvioidaan objekti, tai Object.prototype muuten. Äskettäin luotu objekti on sidottu tähän arvoon funktion paikallisessa ympäristössä. Jos funktio palauttaa objektin, uusi lauseke arvioi kyseisen objektin, muuten uusi lauseke arvioi tämän arvon implisiittisesti luotuksi.

Mitä tulee prototyypin ominaisuuteen, se luodaan epäsuorasti aina, kun määrität tavallisen toiminnon. Jokaisella äskettäin määritellyllä toiminnolla on määritetty ominaisuus nimeltä “prototyyppi”, jonka arvo on vastikään luotu objekti. Tällä esineellä puolestaan ​​on rakentajaominaisuus, joka osoittaa takaisin alkuperäiseen funktioon. Huomaa, että tämä prototyypin ominaisuus ei ole sama kuin [[Prototyyppi]] -paikka. Edellisessä koodiesimerkissä Foo on silti vain funktio, joten sen [[Prototyyppi]] on ennalta määritetty objekti Function.prototype.

Tässä on kaavio, joka kuvaa edellistä koodinäytettä [[Prototyyppi]] -suhteilla mustalla ja omaisuussuhteilla vihreällä ja sinisellä.

edellisen koodinäytteen prototyyppihierarkian kaavio

[1] Voisit mahdollisesti olla esineitä, joilla on [[Rakenna]] -menetelmä ja jolla ei ole [[Soita]] -menetelmää, mutta ECMAScript-määritelmässä ei määritetä sellaisia ​​objekteja. Siksi kaikki rakentajat ovat myös toimintoja.

[2] Tavallisilla funktiomääritelmillä tarkoitan funktioita, jotka on määritelty käyttämällä normaalia funktioavainsanaa eikä mitään muuta sen sijaan, että => funktiot, generaattoritoiminnot, async-funktiot, menetelmät jne. Tietysti ennen ES6: ta tämä oli ainoa tyyppi funktion määritelmä.

Uudet tyylitunnit

Kun tämä tausta on poissa tieltä, on aika tutkia ES6-luokan syntaksia. Edellinen koodinäyte kääntyy suoraan uuteen syntaksiin seuraavasti:

Kuten aikaisemmin, jokainen luokka koostuu konstruktorifunktiosta ja prototyyppiobjektista, jotka viittaavat toisiinsa prototyypin ja konstruktorin ominaisuuksien kautta. Näiden kahden määritelmäjärjestys on kuitenkin päinvastainen. Vanhalla tyyliluokalla määrität konstruktoritoiminnon, ja prototyyppiobjekti luodaan sinulle. Uudella tyyliluokalla luokanmäärittelyn kappaleesta tulee prototyyppiobjektin sisältö (paitsi staattiset menetelmät), ja niiden joukossa määrität konstruktorin. Lopputulos on sama kummallakin tavalla.

Joten jos ES6-luokan syntaksi on vain sokeria vanhan tyylin luokille, mikä on järkeä? Sen lisäksi, että se näyttää paljon mukavammalta ja lisää turvallisuustarkastuksia, uudessa luokkasyntaksissa on myös toiminnallisuus, joka oli mahdoton ennen ES6: ta, erityisesti luokkaperustainen perintö. Kun määrität luokan uudella syntaksilla, voit valinnaisesti antaa superluokan luokalle, joka perii alla olevan osoituksen mukaan:

Tämä esimerkki itsessään voidaan silti jäljitellä ilman luokkasyntaksia, vaikka vaadittava koodi on paljon rumampi.

Luokkaperusteisella perinnöllä sääntö on yksinkertainen - jokaisella parin osalla on prototyyppinsä vastaava osa superluokkaa. Joten superluokan rakentaja on alaluokan konstruktorin [[prototyyppi]] ja superluokan prototyyppiobjekti alaluokan prototyyppiobjektin [[Prototyyppi]]. Tässä on kuvaava kaavio (näytetään vain [[prototyypit]]; ominaisuudet poistetaan selvyyden vuoksi).

Ei ole suoraa ja kätevää tapaa perustaa nämä [[prototyyppi]] -suhteet käyttämättä luokkasyntaksia, mutta voit asettaa ne manuaalisesti käyttämällä ES5: ssä esiteltyä Object.setPrototypeOf ().

Yllä olevassa esimerkissä vältetään kuitenkin tekemästä mitä tahansa rakentajissa. Erityisesti vältetään super, uusi syntaksin pala, joka antaa alaluokille pääsyn superluokan ominaisuuksiin ja rakentajaan. Tämä on paljon monimutkaisempaa, ja sitä on tosiasiassa mahdotonta jäljitellä kokonaan ES5: ssä, vaikkakin se voidaan emuloida ES6: ssa käyttämättä luokkasynnettä tai superäyttöä Reflektin avulla.

Superluokan omaisuus

Superluokan konstruktorin superkutsumiseen tai superluokan ominaisuuksiin pääsyyn on kaksi käyttötapaa. Toinen tapaus on yksinkertaisempi, joten käsittelemme sen ensin.

Tapa, jolla super toimii, on, että jokaisella funktiolla on sisäinen paikka, nimeltään [[HomeObject]], joka pitää kohdetta, jonka sisällä funktio alun perin määriteltiin, jos se määritettiin alun perin menetelmäksi. Luokan määritelmää varten tämä objekti on luokan prototyyppiobjekti, ts. Foo.prototyyppi. Kun käytät ominaisuutta super.foo: n tai super ["foo"]: n kautta, se vastaa [[HomeObject]]. [[Prototyyppi]]. Foo.

Tämän ymmärtämisen avulla, miten super toimii kulissien takana, voit ennustaa, kuinka se toimii jopa monimutkaisissa ja epätavallisissa olosuhteissa. Esimerkiksi funktion [[HomeObject]] on kiinteä määriteltynä ajankohtana, eikä se muutu, vaikka annat toiminnon myöhemmin muille objekteille, kuten alla on esitetty.

Yllä olevassa esimerkissä otimme funktion, joka alun perin määriteltiin D.prototyypissä, ja kopioimme sen B.prototyyppiin. Koska [[HomeObject]] osoittaa edelleen D.prototyypille, superkäyttö näyttää D.prototyypin, joka on C.prototype, [[Prototyyppi]]. Tuloksena on, että C: n kopiosta fooa kutsutaan, vaikka C: tä ei ole missään b: n prototyyppiketjussa.

Samoin se, että [[HomeObject]]. [[Prototyyppi]] tarkastellaan jokaisessa superlausekkeen arvioinnissa, tarkoittaa, että se näkee muutokset [[Prototyyppi]]: een ja tuottaa uusia tuloksia, kuten alla on esitetty.

Sivuhuomautuksena, super ei ole rajoitettu luokan määritelmiin. Sitä voidaan käyttää myös mistä tahansa objektikirjaimissa määritellystä toiminnosta käyttämällä uutta menetelmän lyhennettä, tässä tapauksessa [[HomeObject]] on sulkevan objektin kirjaimellinen. Tietenkin objektikirjaimien [[Prototyyppi]] on aina Object.prototyyppi, joten siitä ei ole kovin hyötyä, ellet määritä prototyyppiä manuaalisesti uudelleen, kuten alla tehdään.

Emuloivat superominaisuuksia

Menetelmillemme ei ole mitään tapaa asettaa [[HomeObject]], mutta voimme jäljitellä sitä vain tallentamalla arvon ja tekemällä erottelutarkkuuden manuaalisesti alla olevan kuvan mukaisesti. Se ei ole niin kätevää kuin vain superkirjoittaminen, mutta ainakin se toimii.

Huomaa, että meidän on käytettävä .call (tätä) varmistaaksemme, että supermenetelmää kutsutaan oikealla arvolla. Jos menetelmällä on ominaisuus, joka varjoittaa Function.prototype.call jostain syystä, voimme sen sijaan käyttää Function.prototype.call.call (foo, this) tai Reflect.apply (foo, this), jotka ovat luotettavampia, mutta sanallisia.

Super staattisissa menetelmissä

Voit käyttää myös staattisia menetelmiä. Staattiset menetelmät ovat samat kuin tavalliset menetelmät paitsi, että ne määritellään ominaisuuksiksi rakentajatoiminnossa prototyyppiobjektin sijasta.

super voidaan emuloida staattisissa menetelmissä samalla tavalla kuin normaalissa menetelmässä. Ainoa ero on, että [[HomeObject]] on nyt rakentajatoiminto prototyyppiobjektin sijaan.

Superrakentajat

Kun tavallisen rakennusfunktion [[Construct]] -menetelmä käynnistetään, uusi objekti luodaan epäsuorasti ja sidottu tähän arvoon toiminnon sisällä. Alaluokan rakentajat noudattavat kuitenkin erilaisia ​​sääntöjä. Tätä arvoa ei ole luotu automaattisesti, ja yrittäminen käyttää tätä johtaa virheeseen. Sen sijaan sinun on soitettava superluokan rakentajalle super (args) kautta. Superluokan konstruktorin tulos sidotaan sitten paikalliseen tähän arvoon, jonka jälkeen voit käyttää sitä alaluokan rakentajassa normaalisti.

Tämä tietenkin tuo esiin ongelmia, jos haluat luoda vanhan tyyliluokan, joka voi toimia oikein yhdessä uusien tyylituntien kanssa. Vanhan tyylilajin alaluokkaamisessa uuteen tyyliluokkaan ei ole ongelmaa, koska perusluokan rakentaja on molemmilla tavoin vain tavallinen rakennustoiminto. Uuden tyyliluokan alaluokittelu vanhalla tyyliluokalla ei kuitenkaan toimi kunnolla, koska vanhan tyylin rakentajat ovat aina perusrakentajia, eikä niillä ole erityistä alaluokan rakentajakäyttäytymistä.

Haasteen konkretisoimiseksi oletetaan, että meillä on uusi tyyliluokan perusta, jonka määritelmää ei tunneta ja jota ei voida muuttaa, ja haluamme alaluokan käyttämättä luokan syntaksia, samalla kun olemme yhteensopivia minkä tahansa Base-koodin kanssa, joka odottaa todellista alaluokkaa.

Ensinnäkin, oletamme, että Base ei käytä välityspalvelimia tai epädeterministisiä laskettuja ominaisuuksia tai mitään muuta outoa, koska ratkaisumme käyttää todennäköisesti Base-ominaisuuksia eri määrä kertoja tai eri järjestyksessä kuin todellinen alaluokka tekisi. , eikä tässä voida tehdä mitään.

Sen jälkeen kysymys tulee siitä, kuinka rakentajan kutsuketju voidaan perustaa. Kuten tavallisissa superominaisuuksissa, voimme saada superluokkarakentajan helposti käyttämällä Object.getPrototypeOf (homeObject) .constructoria. Mutta kuinka vedota siihen? Onneksi voimme käyttää Reflect.construct () -sovellusta manuaalisesti minkä tahansa konstruktoritoiminnon sisäisen [[Construct]] -menetelmän käynnistämiseen.

Tämän sitomisen erityistä käyttäytymistä ei voida jäljitellä, mutta voimme vain sivuuttaa tämän ja käyttää paikallista muuttujaa "todellisen" tämän arvon, nimeltään $ this alla olevassa esimerkissä, tallentamiseen.

Huomaa palautus $ this; yläpuolella oleva rivi. Muista, että jos rakentajatoiminto palauttaa objektin, tätä objektia käytetään uuden lausekkeen arvona implisiittisesti luodun arvon sijasta.

Joten tehtävä suoritettu? Ei aivan. Yllä olevan esimerkin obj-arvo ei oikeastaan ​​ole Child-esimerkki, ts. Sen prototyyppiketjussa ei ole Child.prototyyppiä. Tämä johtuu siitä, että Basin rakentaja ei tiennyt mitään Childista ja palautti siten objektin, joka oli vain selkeä Base-esimerkki (sen [[prototyyppi]] on Base.prototyyppi).

Joten miten tämä ongelma ratkaistaan ​​oikeissa luokissa? [[Construct]], ja laajennuksena Reflect.construct, tosiasiallisesti ottavat kolme parametria. Kolmas parametri, newTarget, on viittaus rakentajaan, jota alun perin kehotettiin uudessa lausekkeessa, ja siten perinnehierarkian alaluokkaan (eniten johdettua) luokan rakentajaa. Kun ohjausvirta saavuttaa perusluokan konstruktorin, implisiittisesti luodulla objektilla on newTarget sen [[Prototyyppi]].

Siksi voimme tehdä Base-konstruktiosta lapsen ilmentymän kutsumalla rakentajaa Reflect.constructin kautta (konstruktori, args, Child). Tämä ei kuitenkaan ole vielä aivan oikein, koska se rikkoutuu aina, kun joku muu alaluokka Lapsi. Lastenluokan koodaamisen sijasta meidän on läpäistävä newTarget-ohjelma muuttumattomana. Onneksi siihen pääsee konstruktoreissa käyttämällä erityistä new.target-syntaksia. Tämä johtaa seuraavaan lopulliseen ratkaisuun:

Viimeinen kosketus

Tämä kattaa kaikki luokkien tärkeimmät toiminnallisuudet, mutta on olemassa joitain muita pieniä eroja, lähinnä turvallisuustarkastukset, jotka on lisätty uuteen luokan syntaksiin. Esimerkiksi funktiomääritelmiin automaattisesti lisätty prototyyppiominaisuus on oletusarvoisesti kirjoitettavissa, mutta luokanrakentajien prototyyppiominaisuus ei ole kirjoitettavissa. Voimme helposti tehdä myös kirjoittamattomasta soittamalla Object.defineProperty (). Vaihtoehtoisesti voit soittaa vain Object.freeze () -sovellukselle, jos haluat, että koko asia on muuttumaton.

Toinen uusi suoja on, että luokan rakentajat heittävät TypeErrorin, jos yrität [[Soita]] heille sen sijaan, että rakentaisit niitä uudella. Yllä oleva rakentajamme sattuu heittämään myös TypeErroria, mutta vain epäsuorasti, koska new.target on määrittelemätön, kun toiminto on [[Call]] ed ja Reflect.construct () heittää TypeErrorin, jos välität nimenomaisesti määrittelemättömänä viimeisenä argumenttina. Koska TypeError on tässä sattumanvarainen, tuloksena oleva virheviesti on melko hämmentävä. Voi olla hyödyllistä lisätä tarkka tarkistus new.targetille, joka aiheuttaa virheen hyödyllisemmällä virhesanomalla.

Joka tapauksessa toivon, että nautit tästä viestistä ja oppit yhtä paljon kuin minä sen tutkimisessa. Yllä olevat tekniikat ovat harvoin hyödyllisiä reaalimaailman koodeissa, mutta on silti tärkeää ymmärtää, kuinka asiat toimivat konepellin alla, jos sinulla on epätavallinen käyttötapa, joka vaatii mustan taian tavoittamista, tai todennäköisemmin, että olet jumissa debug jonkun toisen musta taikuutta.

Loppusanat Jos, kuten minä, sinua ärsyttää Mediumin jättiläinen sulkematon banneri näytön alaosassa, joka kehottaa sinua ilmoittautumaan, tai verkkosivustojen yleinen pyrkimys tehdä niiden sisällön lukeminen mahdollisimman vaikeaksi ja ärsyttäväksi, suosittelen voimakkaasti Killin tarkistamista tahmea. Se on yksinkertainen Javascript-katkelma, jonka voit lisätä kirjanmerkkeihin ja joka poistaa kaikki ”tarttuvat” elementit sivulta. Se kuulostaa yksinkertaiselta, mutta Kill Sticky -selaimessa selaaminen muuttaa elämää. Ja koska kyseessä on vain kirjanmerkki, sinun ei tarvitse huolehtia tärkeiden sivuelementtien vahingossa tapahtuvasta tappamisesta samoin kuin uBlock-suodattimella. Pahimmassa tapauksessa voit aina päivittää sivun.