Kako ES6 časovi stvarno rade i kako izgraditi vlastiti

Šesto izdanje ECMAScript-a (ili ukratko ES6) revolucioniralo je jezik dodajući mnoge nove značajke, uključujući klase i nasljeđivanje temeljeno na klasama. Nova sintaksa lako se koristi bez razumijevanja detalja i uglavnom radi ono što očekujete, ali ako ste poput mene, to nije sasvim zadovoljavajuće. Kako naizgled čarobna sintaksa zapravo djeluje pod haubom? Kako to utječe na ostale značajke u jeziku? Je li moguće oponašati klase bez korištenja sintaksi klase? Evo, odgovorit ću na ta pitanja bez zahvalnosti.

Ali prvo, da biste razumjeli klase, morate razumjeti što je dolazilo prije njih i Javascript's temeljni objektni model.

Model objekta

Model objekta Javascript prilično je jednostavan. Svaki je objekt samo preslikavanje nizova i simbola u deskriptore svojstava. Svaki opisnik svojstava zauzvrat sadrži ili getter / setter par za izračunata svojstva ili vrijednost podataka za obična svojstva podataka.

Kada izvršite kod foo [bar], pretvara bar u niz ako to već nije niz ili simbol, a zatim traži taj ključ među svojstvima foo-a i vraća vrijednost odgovarajućeg svojstva (ili naziva njegovu funkciju dobivanja kao postoji). Za doslovne nizove ključeva koji su valjani identifikatori, postoji kratka sintaksa foo.bar koja je ekvivalentna foo ["bar"]. Za sada tako jednostavno.

Prototipsko nasljeđivanje

Javascript ima ono što se naziva prototipsko nasljeđivanje, što zvuči zastrašujuće, ali zapravo je jednostavnije od tradicionalnog nasljeđivanja zasnovanog na klasičnoj klasiji nakon što ga se spustite. Svaki objekt može imati implicitni pokazivač na drugi objekt, nazvan kao njegov prototip. Kada pokušate pristupiti entitetu objekta u kojem nema svojstva s tim ključem, umjesto toga traži ključ objekta prototipa i vraća svojstvo prototipa za taj ključ ako postoji. Ako ne postoji na prototipu, on rekurzivno provjerava prototip prototipa i tako dalje, sve do lanca dok se ne pronađe svojstvo ili ne postigne objekt bez prototipa.

Ako ste prije koristili Python, postupak pretraživanja atributa je sličan. U Python-u svaki se atribut prvo traži u rječniku primjera. Ako ga nema tamo, vrijeme izvođenja provjerava rječnik klasa, zatim rječnik super klase i tako dalje, sve do hijerarhije nasljeđivanja. U Javascriptu je postupak sličan osim što ne postoji razlika između tipova objekata i predmeta instanci - svaki objekt može biti prototip bilo kojeg drugog objekta. Naravno, u stvarnom svijetu ljudi rijetko koriste tu činjenicu i umjesto toga svoj kôd organiziraju u klase poput hijerarhije, jer je tako lakše upravljati, zbog čega je Javascript na prvom mjestu dodao sintaksu klase.

Unutarnji prorezi

Ako se cijeli objekt sastoji od mapiranja ključeva svojstava, gdje se pohranjuje prototip? Odgovor je da objekti pored svojstava imaju i interne metode i interne utore koji se koriste za implementaciju posebne semantike razine jezika. Unutarnjim se slotovima ne može izravno pristupiti s Javascript koda, ali u nekim slučajevima postoje načini za neizravni pristup njima. Na primjer, objektni prototipovi predstavljeni su utorom [[Prototype]] koji se može čitati i pisati pomoću Object.getPrototypeOf (), odnosno Object.setPrototypeOf (). Prema dogovoru, unutarnji slotovi i metode su napisani u [dvostrukim uglatim zagradama]] kako bi se razlikovali od običnih svojstava.

Časovi starog stila

U ranijim verzijama Javascripta uobičajeno je simulirati klase koristeći kôd na sljedeći način.

Odakle ovo? Odakle potječe prototip? Što novo čini? Kako se ispostavilo, čak i najstarije verzije JavaScripta nisu htjele biti previše nekonvencionalne, pa su uključivale neke sintakse pomoću kojih možete kodirati stvari koje su na neki način bile poput klase.

U tehničkom smislu, funkcije u Javascriptu definirane su pomoću dvije interne metode [[Poziv]] i [[Izgradnja]]. Bilo koji objekt pomoću metode [[Call]] naziva se funkcijom, a svaka funkcija koja dodatno ima metodu [[Construct]] naziva se konstruktor¹. Metoda [[Call]] određuje što će se dogoditi kada objekt pozovete kao funkciju, npr. foo (args), dok [[Construct]] određuje što će se dogoditi kad ga pozovete kao novi izraz, tj. novi foo ili new foo (args).

Za uobičajene definicije funkcija² pozivanje [[Construct]] implicitno će stvoriti novi objekt čiji je [[Prototype]] svojstvo prototipa funkcije konstruktora ako to svojstvo postoji i objekt se vrednuje, ili Object.prototype na drugi način. Novostvoreni objekt vezan je za ovu vrijednost unutar lokalnog okruženja funkcije. Ako funkcija vrati objekt, novi izraz će procijeniti taj objekt, u protivnom, novi izraz procjenjuje na implicitno kreiranu ovu vrijednost.

Što se tiče svojstva prototipa, ono se podrazumijeva kreirano kad god definirate uobičajenu funkciju. Svaka novo definirana funkcija ima svojstvo nazvano „prototip“ definirano na njoj, s novostvorenim objektom kao njegovom vrijednošću. Taj objekt zauzvrat ima svojstvo konstruktora koje upućuje na izvornu funkciju. Imajte na umu da ovo svojstvo prototipa nije isto kao utor [[Prototype]]. U prethodnom primjeru koda Foo je još uvijek samo funkcija, pa je njegov [[prototip]] unaprijed definirani objekt Function.prototype.

Ovdje je dijagram za ilustraciju prethodnog uzorka koda s odnosima [[Prototype]] u crnom i odnosima svojstva u zelenoj i plavoj boji.

dijagram hijerarhije prototipa za prethodni uzorak koda

[1] Objekti se mogu zamisliti metodom [[Konstrukcija]], a ne metodom [[Poziv]], ali ECMAScript specifikacija ne definira nijedne takve objekte. Stoga su i svi konstruktori u funkciji.

[2] Pod uobičajenim definicijama funkcija, podrazumijevam funkcije definirane pomoću ključne riječi regularne funkcije i ništa drugo, osim => funkcije, funkcije generatora, funkcije asinkronizacije, metode itd. Naravno, prije ES6, ovo je bila jedina vrsta definicija funkcije.

Nove klase stila

Kad se takva pozadina odmakne, vrijeme je da istražimo sintaksu klase ES6. Prethodni uzorak koda prevodi se izravno na novu sintaksu kako slijedi:

Kao i prije, svaka klasa se sastoji od funkcije konstruktora i objekta prototipa koji se odnose na svojstva prototipa i konstruktora. Međutim, redoslijed definicije dva je obrnut. Sa starom klasi stila definirate funkciju konstruktora i objekt prototipa je stvoren za vas. S novom vrstom stila tijelo definicije klase postaje sadržaj objekta prototipa (osim statičkih metoda), a među njima definirate konstruktor. Krajnji rezultat je u oba slučaja isti.

Dakle, ako je sintaksa klase ES6 samo šećer za stare "klase", u čemu je smisao? Osim što izgleda puno ljepše i dodaje sigurnosne provjere, sintaksa nove klase ima i funkcionalnost koja je bila nemoguća prije ES6, točnije nasljeđivanje temeljeno na klasi. Kada definirate klasu s novom sintaksom, opcijski možete dati super klasu za klasu koju će naslijediti kao što je prikazano u nastavku:

Ovaj sam primjer i dalje se može oponašati bez sintaksa klase, iako je potreban kôd mnogo ružniji.

Kod nasljeđivanja zasnovanih na klasi pravilo je jednostavno - svaki dio para ima za svoj prototip odgovarajući dio superklase. Dakle, konstruktor nadklase je [[prototip]] konstruktora podrazreda, a prototipni objekt nadklase je [[prototip]] objekta prototipa podrazreda. Evo dijagrama za ilustraciju (prikazani su samo [[prototipovi]]; svojstva su izostavljena radi jasnoće).

Ne postoji izravan i prikladan način za postavljanje ovih odnosa [[prototipa] bez upotrebe klase sintakse, ali možete ih postaviti ručno koristeći Object.setPrototypeOf (), uveden u ES5.

Međutim, gornji primjer posebno izbjegava raditi bilo što na konstruktorima. Konkretno, izbjegava se super, novi dio sintakse koji omogućuje potklastima pristup svojstvima i konstruktoru nadklase. To je mnogo složenije i u stvari je nemoguće u potpunosti emulirati u ES5, iako se može oponašati u ES6 bez korištenja klase sintaksa ili super korištenjem Reflect.

Pristup vlasništvu super klase

Postoje dvije upotrebe za super pozivanje konstruktora super klase ili za pristup svojstvima pretklasa. Drugi je slučaj jednostavniji, pa ćemo ga prvo pokriti.

Super funkcionira tako da svaka funkcija ima unutarnji utor nazvan [[HomeObject]], koji drži objekt u kojem je funkcija izvorno definirana ako je prvotno definirana kao metoda. Za definiciju klase, ovaj će objekt biti prototipni objekt klase, tj. Foo.prototype. Kada pristupite entitetu putem super.foo ili super ["foo"], ono je ekvivalentno [[HomeObject]]. [[Prototype]].

Uz ovo razumijevanje kako super djeluje iza kulisa, možete predvidjeti kako će se ponašati čak i pod kompliciranim i neobičnim okolnostima. Na primjer, funkcija [[HomeObject]] fiksirana je u vrijeme definicije i neće se promijeniti čak i ako kasnije dodijelite funkciju drugim objektima kao što je prikazano u nastavku.

U gornjem primjeru uzeli smo funkciju koja je izvorno definirana u D.prototype i kopirali smo je u B.prototype. Budući da [[HomeObject]] još uvijek ukazuje na D.prototip, super pristup izgleda u [prototipu] D.prototipa, koji je C.prototype. Rezultat toga je što se zove C kopija fooa iako C nigdje u lancu prototipa tvrtke B.

Isto tako, činjenica da je [[HomeObject]]. [[Prototype]] pregledan kod svake procjene super izraza znači da će vidjeti promjene u [[Prototype]] i vratiti nove rezultate, kao što je prikazano u nastavku.

Kao sporedna napomena, super nije ograničen na definicije klasa. Također se može koristiti iz bilo koje funkcije definirane u objektnom literalu koristeći novu metodu kratkoročne sintakse, a u tom slučaju [[HomeObject]] će biti objekt koji se prilaže. Naravno, [[prototip]] doslovnog predmeta uvijek će biti Object.prototype, tako da nije pretjerano korisno ako ručno ne dodijelite prototip kao što je dolje učinjeno.

Emuliranje super svojstava

Nema načina da ručno postavimo [[HomeObject]] na naše metode, ali to možemo oponašati samo spremanjem vrijednosti i ručnom odlučivanjem kao što je prikazano u nastavku. Nije tako jednostavno kao pisanje super, ali barem tako djeluje.

Imajte na umu da moramo koristiti .call (ovo) kako bismo osigurali da se super metoda zove s ovom ispravnom vrijednošću. Ako metoda ima svojstvo koje zasjenjuje Function.prototype.call iz nekog razloga, umjesto toga možemo upotrijebiti Function.prototype.call.call (foo, this) ili Reflect.apply (foo, this), koji su pouzdaniji, ali višestruki.

Super u statičkim metodama

Također možete koristiti super od statičkih metoda. Statičke metode su iste kao i uobičajene metode, osim što su definirane kao svojstva u funkciji konstruktora, a ne na objektu prototipa.

super se može oponašati statičkim metodama na isti način kao kod uobičajenih metoda. Jedina je razlika što je [[HomeObject]] funkcija konstruktora, a ne objekt prototipa.

Super konstruktori

Kada se poziva [[Construct]] metoda uobičajene konstruktorske funkcije, novi se objekt implicitno kreira i veže uz ovu vrijednost unutar funkcije. Međutim, konstruktori podrazreda slijede različita pravila. Ta se vrijednost ne stvara automatski, a pokušaj pristupa ovim rezultatima dovodi do pogreške. Umjesto toga, morate nazvati konstruktora nadklase putem super (args). Rezultat konstruktora nadklase tada se vezuje za lokalnu ovu vrijednost, nakon čega joj možete pristupiti u konstruktoru podrazreda kao uobičajeni.

Ovo naravno predstavlja probleme ako želite stvoriti staru klasu stila koja može pravilno surađivati ​​s novim klasama stilova. Nema problema kada podklasificiranje stare slojeve klase s novom stilskom klasom, jer je konstruktor osnovne klase samo obična konstruktorska funkcija. No, podklasifikacija nove klase stila sa starom klasu stila neće raditi ispravno, jer konstruktori starog stila uvijek su konstruktori baza i nemaju posebno ponašanje konstruktora podrazreda.

Da bi izazov bio konkretan, pretpostavimo da imamo novu Bazu klasa stila čija je definicija nepoznata i ne može se mijenjati, a želimo je podklasirati bez upotrebe sintaksa klase, a ostaje kompatibilna s bilo kojim kôdom u Baseu koji očekuje pravi podklasa.

Kao prvo, pretpostavit ćemo da Base ne koristi proxyje, ili neterminerirane proračunske osobine ili bilo što drugo čudno, jer će naše rješenje pristupiti svojstvima Base-a drugačiji broj puta ili drugačijim redoslijedom nego što bi to imao stvarni podrazred i ne možemo ništa učiniti po tom pitanju.

Nakon toga postavlja se pitanje kako postaviti lanac poziva konstruktora. Kao i kod redovnih super svojstava, lako možemo dobiti konstruktor superklase koristeći Object.getPrototypeOf (homeObject) .constructor. Ali kako to pozvati? Srećom, pomoću Reflect.construct () možemo ručno pozvati internu [[Construct]] metodu bilo koje funkcije konstruktora.

Ne postoji način oponašanja posebnog ponašanja ovog vezanja, ali to možemo jednostavno ignorirati i koristiti lokalnu varijablu za pohranjivanje "stvarne" ove vrijednosti, nazvane $ this u primjeru u nastavku.

Zabilježite povrat $ this; linija iznad. Podsjetimo da ako funkcija konstruktora vrati objekt, taj će se objekt upotrijebiti kao vrijednost novog izraza umjesto implicitno stvorene ove vrijednosti.

Dakle, misija ostvarena? Ne baš. Vrijednost obj u gornjem primjeru zapravo nije instanca Child-a, tj. Nema Child.prototype u svom prototipskom lancu. To je zato što Baseov konstruktor nije znao ništa o Child-u pa je vratio objekt koji je bio samo obična instanca Base (njegov [[prototip]] je Base.prototype).

Pa kako se ovaj problem rješava za prave časove? [[Construct]] i ekstenzijom Reflect.construct zapravo uzimaju tri parametra. Treći parametar, newTarget, odnosi se na konstruktor koji je izvorno pozvan u novom izrazu, a samim tim i konstruktor najniže (najviše izvedene) klase u hijerarhiji nasljeđivanja. Nakon što kontrolni tijek dosegne konstruktor osnovne klase, implicitno stvoreni ovaj objekt imat će newTarget kao svoj [[Prototype]].

Stoga možemo napraviti Base da konstruira instancu Child-a tako što ćemo pozvati konstruktor putem Reflect.construct (konstruktor, args, Child). Međutim, to još uvijek nije sasvim u redu, jer će se pokvariti kad god netko podklasira Child. Umjesto tvrdog kodiranja dječje klase, moramo proći kroz novi cilj nepromijenjeni. Srećom, može se pristupiti unutar konstruktora koristeći posebnu sintaksu new.target. Ovo dovodi do konačnog rješenja u nastavku:

Završni dodiri

Ovo obuhvaća sve glavne funkcionalnosti klasa, ali postoji nekoliko drugih manjih razlika, uglavnom sigurnosnih provjera dodanih u sintaksu nove klase. Na primjer, svojstvo prototipa automatski dodano definicijama funkcija može se zadati prema upisu, ali svojstvo prototipa konstruktora klase nije moguće upisati. Jednostavno možete učiniti i naše neobjavljivim pozivanjem Object.defineProperty (). Alternativno, možete nazvati Object.freeze () ako želite da cijela stvar bude nepromjenljiva.

Još jedna nova zaštita je da će konstruktori klase izbaciti TypeError ako ih pokušate [[Call]] zamijeniti umjesto da ih konstruirate s novim. Naš gornji konstruktor događa se i da baca TypeError, ali samo neizravno, jer je new.target nedefiniran kada je funkcija [[Call]] ed, a Reflect.construct () baca TypeError ako izričito proslijedite undefined kao zadnji argument. Budući da je ovdje TypeError slučajni, rezultirajuća poruka o pogrešci poprilično je zbunjujuća. Bilo bi korisno dodati eksplicitni ček za new.target koji baca pogrešku s korisnijom porukom o pogrešci.

U svakom slučaju, nadam se da ste uživali u ovom postu i naučili ste koliko i ja u procesu njegovog istraživanja. Gore navedene tehnike rijetko su korisne u stvarnom svjetskom kodu, no svejedno je važno razumjeti kako stvari funkcioniraju pod haubom u slučaju da imate neobičan slučaj upotrebe koji zahtijeva posezanje za crnom magijom ili, što je vjerojatnije, zaglavili ste da ispraviti tuđu crnu magiju.

p.s. Ako vas, poput mene, nerviraju divovski neizbrisivi natpis na dnu zaslona koji vas poziva da se prijavite ili općenita potraga web stranica da čitanje njihovog sadržaja bude što teže i neugodnije, toplo bih preporučio da provjerite Kill Ljepljiv. To je jednostavan Javascript isječak na kojem možete staviti oznaku koja briše sve "ljepljive" elemente na stranici. Zvuči jednostavno, ali pregledavanje Kill Sticky-a život se mijenja. A budući da je riječ samo o knjižkoj knjižici, ne morate brinuti da ćete slučajno ubiti važne elemente stranice za dobro kao što biste to učinili s uBlock filterom. U najgorem slučaju uvijek možete samo osvježiti stranicu.