Kuidas ES6 klassid tegelikult töötavad ja kuidas omaenese üles ehitada

ECMAScripti 6. väljaanne (või lühidalt ES6) pani keele pöörde, lisades palju uusi funktsioone, sealhulgas klassid ja klassipõhine pärand. Uut süntaksit on lihtne kasutada ilma detaile mõistmata ja enamasti tehakse seda, mida võiksite oodata, kuid kui olete minusugune, pole see just kuigi rahuldav. Kuidas tegelikult pealtnäha maagiline süntaks kapoti all tegelikult töötab? Kuidas see suhestub keele muude funktsioonidega? Kas on võimalik klasse jäljendada ilma klassi süntaksit kasutamata? Siinkohal vastan neile küsimustele tasuta üksikasju.

Kuid kõigepealt peate klasside mõistmiseks mõistma, mis neile ette jõudis, ja Javascripti aluseks olevat objekti mudelit.

Objekti mudel

Javascripti objektimudel on üsna lihtne. Iga objekt on lihtsalt stringide ja sümbolite kaardistamine omaduste kirjeldajatega. Igas atribuutide kirjelduses on omakorda arvutatud omaduste jaoks getter / setteripaar või tavaliste andmeomaduste jaoks andmeväärtus.

Koodi foo [bar] käivitamisel teisendab see riba stringiks, kui see pole veel string ega sümbol, seejärel otsib selle võtme foo atribuutide hulgast ja tagastab vastava omaduse väärtuse (või nimetab selle getterfunktsiooni kui kohaldatav). Kehtivate identifikaatoritega sõnasõnaliste stringiklahvide jaoks on lühisüntaks foo.bar, mis on samaväärne foo-ga ["bar"]. Siiani nii lihtne.

Prototüüpse pärimine

Javascriptis on nn prototüüpseks päranduseks, mis kõlab hirmutavalt, kuid on tegelikult lihtsam kui traditsiooniline klassipõhine pärand, kui saate selle riputada. Igal objektil võib olla kaudne osutus mõnele teisele objektile, mida nimetatakse selle prototüübiks. Kui proovite juurde pääseda objekti atribuudile, kus selle võtmega pole ühtegi omadust, otsib see selle asemel prototüübi objekti võtit ja tagastab selle võtme prototüübi atribuudi, kui see on olemas. Kui seda prototüübis ei eksisteeri, kontrollib see rekursiivselt prototüübi prototüüpi ja nii edasi, kogu ahelas ülespoole, kuni atribuut leitakse või objekt ilma prototüübita.

Kui olete Pythonit varem kasutanud, on atribuudi otsimise protsess sarnane. Pythonis otsitakse igat atribuuti esmalt eksemplari sõnaraamatust. Kui seda seal pole, kontrollib käitusaeg klassisõnastikku, seejärel superklassi sõnastikku jne, pärandihierarhiast ülespoole. Javascriptis on protsess sarnane, välja arvatud see, et tüüpobjekte ja eksemplariobjekte ei tehta vahet - iga objekt võib olla mis tahes muu objekti prototüüp. Muidugi, reaalses maailmas kasutavad inimesed seda fakti harva ja organiseerivad selle asemel oma koodi klassitaolistesse hierarhiatesse, kuna seda on lihtsam hallata, mistõttu Javascript lisas klassi süntaksi.

Sisemised teenindusajad

Kui kõik objekt koosneb on omaduste võtmete kaardistamine, siis kuhu prototüüp salvestatakse? Vastus on, et lisaks omadustele on objektidel ka sisemised meetodid ja sisemised pesad, mida kasutatakse spetsiaalse keeletaseme semantika rakendamiseks. Sisemistele teenindusaegadele pole Javascripti koodist otse juurde pääseda, kuid mõnel juhul on kaudsele juurdepääsule võimalusi. Näiteks tähistatakse objekti prototüüpe pesaga [[Prototype]], mida saab lugeda ja kirjutada vastavalt objektidega Object.getPrototypeOf () ja Object.setPrototypeOf (). Tavaliselt kirjutatakse sisemised pesad ja meetodid [[topelt nurksulud]]], et neid tavalistest omadustest eristada.

Vana stiili tunnid

Javascripti varasemates versioonides oli tavaline klasside simuleerimine, kasutades järgmist koodi.

Kust see tuli? Kust prototüüp tuli? Mida uus teeb? Nagu selgub, ei tahtnud isegi Javascripti kõige varasemad versioonid olla liiga tavapärased, seega sisaldasid nad mõnda süntaksit, mis lubab teil kodeerida asju, mis olid sarnased klassid.

Tehnilises mõttes on Javascripti funktsioonid määratletud kahe sisemise meetodi [[Call]] ja [[Construct]] abil. Igasugust objekti, millel on meetod [[Call]], nimetatakse funktsiooniks ja iga funktsiooni, millel on lisaks [[Construct]] meetod, nimetatakse konstruktoriks1. [[Helista]] meetod määrab, mis juhtub, kui kutsute objekti funktsioonina, nt. foo (args), samas kui [[Construct]] määrab, mis juhtub, kui kutsute selle uue avaldisena, st uue foo või uue foo (args).

Tavaliste funktsioonide definitsioonide² korral loob [[Construct]] -i kutsumine kaudselt uue objekti, mille [[Prototype]] on konstruktorifunktsiooni prototüübi omadus, kui see omadus on olemas ja seda hinnatakse objekti korral, või Object.prototüüp muul juhul. Äsja loodud objekt seob selle väärtuse funktsiooni kohalikus keskkonnas. Kui funktsioon tagastab objekti, hindab uus avaldis seda objekti, vastasel korral hindab uus avaldis selle väärtuse kaudselt loodud väärtuseks.

Mis puutub prototüübi atribuuti, siis see luuakse kaudselt alati, kui määratlete tavalise funktsiooni. Igal äsja määratletud funktsioonil on omadus nimega “prototüüp”, mille väärtus on äsja loodud objekt. Sellel objektil on omakorda konstruktori omadus, mis osutab tagasi algsele funktsioonile. Pange tähele, et see prototüübi omadus pole sama kui [[Prototüüp]] pesa. Eelmises koodinäites on Foo endiselt vaid funktsioon, seega on selle [[Prototüüp]] etteantud objekt Funktsioon.prototüüp.

Siin on diagramm, mis illustreerib eelmist koodinäidist koos [[Prototüübi]] suhetega mustas ja omandisuhetes rohelise ja sinisega.

eelmise koodinäidise prototüübi hierarhia skeem

[1] Võimalik, et teil on objekte, millel on meetod [[Construct]] ja puudub [[Call]] meetod, kuid ECMAScripti spetsifikatsioon selliseid objekte ei määratle. Seetõttu on ka kõik konstruktorid funktsioonid.

[2] Tavaliste funktsioonimääratluste all pean ma silmas funktsioone, mis on määratletud tavalise funktsiooni märksõna abil ja mitte millegi muu asemel, kui => funktsioone, generaatori funktsioone, asünkroofunktsioone, meetodeid jne. Muidugi, enne ES6 oli see ainus funktsiooni määratlus.

Uued stiilitunnid

Kuna see taust on takistusteta, on aeg uurida ES6 klassi süntaksit. Eelmine koodinäide tõlgitakse otse uude süntaksi järgmiselt:

Nagu varem, koosneb iga klass konstruktorifunktsioonist ja prototüübiobjektist, mis prototüübi ja konstrukatori omaduste kaudu üksteisele viitavad. Nende kahe määratluse järjekord on aga vastupidine. Vana stiiliklassi abil määratlete konstruktori funktsiooni ja prototüüpobjekt luuakse teile. Uue stiiliklassiga saab klassimääratluse põhiosa prototüübiobjekti sisuks (välja arvatud staatilised meetodid) ja nende hulgast määratlete konstruktori. Lõpptulemus on mõlemal juhul sama.

Nii et kui ES6 klassi süntaks on lihtsalt vanas stiilis klasside suhkur, mis on selle mõte? Lisaks palju ilusamale vaatamisele ja ohutuskontrollide lisamisele on uues klassi süntaksis ka funktsionaalsus, mis oli ES6-eelselt võimatu, täpsemalt klassipõhine pärand. Kui määratlete klassi uue süntaksi abil, võite soovi korral pakkuda klassile superklassi, millelt ta pärib, nagu allpool näidatud:

Seda näidet saab iseenesest jäljendada ilma klassisüntaksita, ehkki vajalik kood on palju koledam.

Klassipõhise pärimise puhul on reegel lihtne - iga paari osa prototüübiks on superklassi vastav osa. Nii et superklassi konstruktor on alamklassi konstruktori [[prototüüp]] ja ülemklassi prototüübiobjekt on alaklassi prototüüpobjekti [[Prototüüp]]. Siin on illustreeriv diagramm (näidatud on ainult [[prototüübid]]; omadused on selguse huvides ära jäetud).

Nende [[Prototüübi]] suhete seadistamiseks ilma klassi süntaksit kasutamata pole otsest ja mugavat viisi, kuid saate need käsitsi seadistada, kasutades ES5-s tutvustatud objekti Object.setPrototypeOf ().

Ülaltoodud näites välditakse eriti konstruktorites midagi tegemast. Eelkõige välditakse super süntaksi uut osa, mis võimaldab alaklassidel pääseda juurde superklassi omadustele ja konstruktorile. See on palju keerulisem ja seda on ES5-s võimatu täielikult jäljendada, ehkki seda saab ES6-s jäljendada ilma klassisüntaksit või superdetaili kasutades Reflect.

Juurdepääs superklassile

Superklassi konstruktori kutsumiseks või superklassi omadustele juurdepääsu saamiseks on kaks kasutusviisi. Teine juhtum on lihtsam, nii et käsitleme seda kõigepealt.

Superfunktsioon on see, et igal funktsioonil on sisemine pesa nimega [[HomeObject]], mis hoiab objekti, milles funktsioon algselt määratleti, kui see oli algselt määratletud meetodina. Klassimääratluse jaoks on see objekt klassi prototüübiobjekt, st Foo.prototüüp. Kui pääsete atribuudile super.foo või super ["foo"] kaudu, on see samaväärne [[HomeObject]]. [[Prototype]]. Foo.

Selle mõistmise abil, kuidas super toimib kulisside taga, saate ennustada, kuidas see käitub isegi keerulistes ja ebaharilikes olukordades. Näiteks funktsiooni [[HomeObject]] on fikseeritud määratluse ajal ja see ei muutu isegi siis, kui määrate funktsiooni hiljem teistele objektidele, nagu allpool näidatud.

Ülaltoodud näites võtsime funktsiooni, mis oli algselt määratletud prototüübis, ja kopeerisime selle üle B. prototüübi. Kuna [[HomeObject]] osutab endiselt D. prototüübile, vaatab superpääs D. prototüübi, mis on C. prototüüp, [[Prototüüp]]. Tulemuseks on see, et C eksoo koopiat nimetatakse isegi siis, kui C pole kuskil b prototüübi ahelas.

Samamoodi tähendab asjaolu, et [[HomeObject]]. [[Prototüüp]] vaadatakse üle igal üliväljenduse hindamisel, et see näeb muutusi [[Prototüüp]] ja annab uusi tulemusi, nagu allpool näidatud.

Kõrvalmärkusena ei piirdu super klasside määratlustega. Seda saab kasutada ka mis tahes funktsioonis, mis on määratletud objektikirjalises sõnas, kasutades uue meetodi lühendatud süntaksit, sel juhul on [[HomeObject]] ümbritseva objekti tähtsõna. Muidugi on objektikirjaliste [[Prototüüp]] alati Object.prototüüp, nii et see pole eriti kasulik, kui te ei määra prototüüpi käsitsi ümber, nagu allpool kirjeldatud.

Superomaduste jäljendamine

Meie meetoditel pole [[HomeObject]] käsitsi seadistatav, kuid saame seda jäljendada, lihtsalt salvestades väärtuse ja tehes eraldusvõime käsitsi, nagu allpool näidatud. See pole nii mugav kui lihtsalt super kirjutamine, kuid vähemalt töötab.

Pange tähele, et me peame kasutama .call (seda), et tagada supermeetodi õige väärtus selle väärtusega. Kui meetodil on omadus, mis varjutab Function.prototype.call mingil põhjusel, võiksime selle asemel kasutada Function.prototype.call.call (foo, see) või Reflect.apply (foo, this), mis on usaldusväärsemad, kuid paljusõnalised.

Super staatilistes meetodites

Võite kasutada ka staatiliste meetodite super. Staatilised meetodid on samad, mis tavalised meetodid, välja arvatud see, et need määratletakse prototüüpobjekti asemel konstruktori funktsiooni omadustena.

super saab jäljendada staatiliste meetodite abil samamoodi nagu tavaliste meetodite puhul. Ainus erinevus on see, et [[HomeObject]] on nüüd konstruktori funktsioon, mitte prototüübi objekt.

Superkonstruktorid

Kui kutsutakse välja tavalise konstruktori funktsiooni [[Construct]] meetod, luuakse vaikimisi uus objekt ja seotakse selle funktsiooni sees oleva väärtusega. Alamklassi konstruktorid järgivad aga erinevaid reegleid. Seda väärtust pole automaatselt loodud ja sellele juurde pääseda üritatakse tõrkega. Selle asemel peate superklassi ehitaja kutsuma super (args) kaudu. Superklassi konstruktori tulemus seotakse seejärel kohaliku väärtusega, mille järel saate sellele juurde pääseda alaklassi konstruktoris nagu tavaliselt.

See tekitab muidugi probleeme, kui soovite luua vana stiiliklassi, mis saaks uute stiilitundidega korralikult koostööd teha. Vana stiiliklassi alamklassimisel uue stiiliklassiga pole probleeme, kuna põhiklassi konstruktor on mõlemal juhul tavaline konstrukatori funktsioon. Uue stiiliklassi alamklassi klassifitseerimine vana stiiliklassiga ei toimi aga korralikult, kuna vana stiili konstruktorid on alati põhiehitajad ja neil pole erilist alaklassi konstruktori käitumist.

Väljakutse konkreetseks muutmiseks oletame, et meil on uus stiiliklass Base, mille määratlus pole teada ja mida ei saa muuta, ning soovime selle alamklassi klassisüntaksit kasutamata, jäädes samal ajal ühilduvaks Base'i mis tahes koodiga, mis ootab tõelist alaklassi.

Esiteks eeldame, et Base ei kasuta puhverservereid ega mittedeterministlikke arvutatud atribuute ega midagi muud imelikku, kuna meie lahendus pääseb Base'i atribuutidele tõenäoliselt mitu korda või erinevas järjekorras kui reaalne alaklass. , ja selles ei saa midagi teha.

Pärast seda saab küsimus, kuidas ehitaja kõneahel üles seada. Nagu tavaliste superomaduste korral, saame ka superklassi konstruktori hõlpsalt ehitajaga Object.getPrototypeOf (homeObject). Aga kuidas seda kutsuda? Õnneks saame kasutada Reflect.construct (), et käsitsi käivitada mis tahes konstruktori funktsiooni sisemine [[Construct]] meetod.

Selle köitmise erilist käitumist ei saa kuidagi jäljendada, kuid võime seda lihtsalt ignoreerida ja kasutada selle väärtuse tegeliku väärtuse salvestamiseks kohalikku muutujat, mille allpool toodud näites nimetatakse $ this.

Pange tähele tagastamist $ this; joon ülal. Pidage meeles, et kui konstruktorifunktsioon tagastab objekti, kasutatakse seda objekti vaikimisi loodud väärtuse asemel uue avaldise väärtusena.

Niisiis, missioon on täidetud? Mitte päris. Ülaltoodud näite obj väärtus ei ole tegelikult Childi näide, st selle prototüübi ahelas pole Child.prototüüpi. Selle põhjuseks on asjaolu, et Base'i konstruktor ei teadnud lapsest midagi ja saatis seetõttu tagasi objekti, mis oli lihtsalt Base tavaline näide (selle [[prototüüp]] on Base.prototüüp).

Niisiis, kuidas see probleem reaalainete tundides lahendatakse? [[Construct]] ja laiendusena Reflect.construct võtavad tegelikult kolm parameetrit. Kolmas parameeter newTarget on viide konstruktorile, millele algselt kutsuti uues avaldises, ja seega pärimishierarhia kõige madalama (kõige tuletatud) klassi konstruktorile. Kui juhtimisvoog jõuab põhiklassi konstruktorini, on selle objekti kaudselt loodud objekt newTarget selle [[Prototüüp]].

Seetõttu võime baaskonstruktsiooni muuta lapse eksemplariks, kutsudes konstruktori Reflect.construct (konstruktor, args, laps) kaudu. Kuid see pole ikka veel päris õige, sest see puruneb iga kord, kui keegi teine ​​alamklassi Child. Lasteklassi kodeerimise asemel peame läbima newTargeti muutmata kujul. Õnneks pääseb sellele konstruktorites sisse spetsiaalse süntaksi new.target abil. See viib järgmise lahenduseni:

Viimane puudutus

See hõlmab kõiki peamisi klasside funktsionaalsusi, kuid on ka mõned muud väikesed erinevused, enamasti on uue klassi süntaksile lisatud ohutuskontrollid. Näiteks funktsioonimääratlustele automaatselt lisatud prototüübi atribuut on vaikimisi kirjutatav, kuid klassi konstruktorite prototüübiomadus pole kirjutatav. Saame hõlpsasti muuta ka meie kirjutamatuks, helistades objektile Object.defineProperty (). Teise võimalusena võite helistada lihtsalt Object.freeze (), kui soovite, et kogu asi oleks muutumatu.

Veel üks uus kaitse on see, et klassi konstruktorid viskavad TypeErrori, kui proovite neile [[Helista]] helistada, selle asemel, et neid uutega ehitada. Meie ülaltoodud konstruktoriga juhtub ka TypeErrorit viskama, kuid ainult kaudselt, kuna funktsiooni [[Helista]] redigeerimiseks pole new.target defineeritud ja Reflect.construct () viskab TypeErrori, kui te viimase argumendina otsustate määratleda. Kuna TypeError on siin juhuslik, on sellest tulenev veateade üsna segane. Võib olla kasulik lisada selgem kontroll new.target'i kohta, mis tõrjub vea koos kasulikuma veateatega.

Igatahes loodan, et teile meeldis see postitus ja õppisite sama palju kui mina selle uurimise käigus. Ülaltoodud tehnikad on reaalmaailma koodides harva kasulikud, kuid siiski on oluline mõista, kuidas asjad kapoti all toimivad, kui teil on ebaharilik kasutusjuhtum, mis nõuab musta maagia poole jõudmist, või tõenäolisem, et olete ummikus siluda kellegi teise musta maagiat.

P.S. Kui mind, nagu mind, ärritab Mediumi hiiglaslik mittekinnitatav ribareklaam ekraani allosas, mis kutsub teid üles registreeruma, või veebisaitide üldised otsingud muuta nende sisu lugemine võimalikult keeruliseks ja tüütuks, soovitaksin tungivalt vaadata Killu Kleepuv. See on lihtne Javascripti koodilõik, mille saate järjehoidjatena kustutada kõik lehe kleepuvad elemendid. See kõlab lihtsalt, kuid Kill Sticky abil sirvimine muudab elu. Ja kuna see on lihtsalt järjehoidja, ei pea te muretsema oluliste leheelementide tahtmatu tapmise pärast, nagu teeksite uBlock-filtriga. Halvimal juhul saate lehte lihtsalt värskendada.