ES6 sinifləri həqiqətən necə işləyir və öz siniflərinizi necə qurursunuz

ECMAScript'in 6-cı nəşri (və ya qısaca ES6) dildə inqilab etdi və siniflər və sinif əsaslı miras da daxil olmaqla bir çox yeni xüsusiyyət əlavə etdi. Yeni sintaksisin detalları başa düşülmədən istifadəsi asandır və əksər hallarda gözlədiyinizi yerinə yetirir. Ancaq mənim kimi olsanız, bu tamamilə qənaətbəxş deyil. Göründüyü kimi sehrli sintaksis başlıq altında necə işləyir? Dildəki digər funksiyalarla necə əlaqə qurur? Sinif sintaksis olmadan dərsləri təqlid etmək mümkündürmü? Bu suallara burada ətraflı cavab verəcəyəm.

Lakin dərsləri anlamaq üçün əvvəlcə onlardan əvvəl nə olduğunu və Javascript-in hansı obyekt modelinə əsaslandığını anlamalısınız.

Obyekt modeli

Javascript obyekt modeli olduqca sadədir. Hər bir obyekt yalnız iplərin və simvolların əmlak təsvirlərinə uyğunlaşdırılmasıdır. Hər bir əmlakın təsviri öz növbəsində ya hesablanmış xüsusiyyətlər üçün bir alıcı / qurucu cütü, ya da adi məlumat xüsusiyyətləri üçün bir məlumat dəyərini ehtiva edir.

Kodu foo [bar] çalıştırdığınız zaman, bir simli və ya bir simvol deyilsə, çubuğu bir simli çevirir. Daha sonra foo xüsusiyyətlərində bu açarı axtarır və müvafiq mülkiyyətin dəyərini qaytarır (və ya getter funksiyasını uyğun olaraq çağırır). Düzgün identifikatorlar olan hərfli sətir düymələri foo ["bar"] - a bərabər olan foo.bar qısa sintaksisinə malikdir. İndiyə qədər çox asandır.

Prototip miras

Javascriptdə prototipik miras kimi tanınan, qorxunc səslənən, ancaq əslində ənənəvi sinif əsaslı mirasdan daha asandır. Hər bir obyekt prototip adlanan başqa bir obyekt üçün gizli bir göstəriciyə sahib ola bilər. Bu açarı ilə bir xüsusiyyəti olmayan bir obyekt üzərində bir mülkə girməyə çalışırsınızsa, bunun əvəzinə prototip obyekt üçün açar tapılır və əgər varsa, bu düymənin prototip xüsusiyyəti qaytarılır. Prototipdə yoxdursa, prototipin prototipi bir xüsusiyyət tapılana və ya prototipi olmayan bir obyektə çatana qədər rekursiv şəkildə yoxlanılır və s.

Daha əvvəl Python istifadə etmisinizsə, atribut axtarışı prosesi oxşardır. Python'da hər bir atribut əvvəlcə nümunə lüğətində axtarılır. Orada yoxdursa, iş vaxtı sinif lüğətini, super sinif lüğətini və s. Miras iyerarxiyasına qədər yoxlayır. Javascript-də, proses oxşardır, yalnız tip obyektlər və instansiya obyektləri arasında heç bir fərq yoxdur - hər obyekt başqa bir obyektin prototipi ola bilər. Aydındır ki, real dünyada insanlar bu həqiqəti nadir hallarda istifadə edirlər və bunun əvəzinə istifadə etmək daha asan olduğu üçün kodlarını sinifə bənzər hiyerarşilərdə təşkil edirlər, bu səbəbdən JavaScript ilk növbədə sinif sintaksisini əlavə etdi.

Daxili yuvalar

Bir obyekt yalnız xüsusiyyətlərin açarlarının bir xəritəsidirsə, prototip harada saxlanılır? Cavab budur ki, xüsusiyyətlərə əlavə olaraq obyektlər dil səviyyəsində xüsusi semantikanı tətbiq etmək üçün istifadə olunan daxili metodlara və daxili yuvalara sahibdirlər. Daxili slotlara heç bir şəkildə birbaşa JavaScript kodu vasitəsilə daxil olmaq mümkün deyil. Lakin, bəzi hallarda dolayı yolla ona çatmağın yolları var. Obyekt prototipləri, məsələn, Object.getPrototypeOf () və ya Object.setPrototypeOf () ilə oxuna və yazıla bilən yuva [[Prototip]] ilə təmsil olunur. Daxili yuvalar və metodlar, ümumi xüsusiyyətlərindən ayırmaq üçün, default olaraq cüt kvadrat mötərizəyə daxil edilmişdir.

Köhnə stil dərsləri

Javascript-in əvvəlki versiyalarında dərsləri aşağıdakı kodla simulyasiya etmək adi hal idi.

Haradan gəldi? Prototip haradan gəldi? Yeniliklər nədir? Məlum olduğu kimi, Javascript-in ən erkən versiyaları belə qeyri-ənənəvi olmaq istəmirdi. Beləliklə, siniflərə bənzər şeyləri kodlaşdırmaq üçün istifadə edə biləcəyiniz sintaksisi daxil etdilər.

Texniki baxımdan Javascriptdəki funksiyalar iki daxili metod [[Çağırış]] və [[İnşaat]] ilə müəyyən edilir. [[Çağırış]] metodu olan hər bir obyektə funksiya deyilir və [[Konstrukt]] metodunu da ehtiva edən hər bir funksiyaya konstruktor¹ deyilir. [[Çağırış]] metodu, bir obyekti funksiya olaraq çağırdığınızda nə baş verdiyini təyin edir, e. foo (args), [[Construct]] isə onu yeni bir ifadə, yəni yeni foo və ya new foo (args) adlandırdığınız zaman nə olacağını təyin edir.

Adi funksiya tərifləri üçün, [[Konstruksiya]] çağırışı, [[Prototip]] konstruktor funksiyasının prototip xassəsi olan yeni bir obyekt yaradır, əgər bu xüsusiyyət mövcuddursa və bir obyekt dəyərinə sahibdirsə, əks halda Object.prototype. Yeni yaradılan obyekt, funksiyanın lokal mühitində bu dəyərə bağlıdır. Əgər funksiya bir obyekti qaytararsa, yeni ifadə həmin obyektə qarşı qiymətləndirilir; əks halda yeni ifadə örtüklə yaradılan dəyərə görə qiymətləndirilir.

Prototip xassəsi adi bir funksiyanı təyin etdiyiniz zaman dolayısı ilə yaradılır. Hər yeni təyin olunmuş funksiyada dəyəri yeni yaradılmış bir obyekt olan "Prototip" adlı bir xüsusiyyət təyin olunur. Bu obyekt, öz növbəsində, orijinal funksiyaya istinad edən bir konstruktor xüsusiyyətinə malikdir. Bu prototip xassəsinin [[Prototip]] yuvası ilə eyni olmadığını unutmayın. Əvvəlki kod nümunəsində Foo sadəcə bir funksiyadır, buna görə prototip daxili Function.prototype obyektidir.

Əvvəlki kod nümunəsini qara [[Prototip]] münasibətləri ilə yaşıl və mavi rəng münasibətləri ilə təsvir edən bir diaqram.

Əvvəlki kod nümunəsi üçün prototip iyerarxiya diaqramı

[1] Mümkündür ki, [[Çağırış]] metoduna deyil, [[İnşa et]] metoduna sahib obyektləriniz olsun, lakin ECMAScript spesifikasiyası bu cür obyektləri təyin etmir. Buna görə bütün konstruktorlar funksiyadır.

[2] Adi funksiya tərifləri dedikdə, normal funksiya açar sözü ilə müəyyən edilmiş funksiyaları nəzərdə tuturam və => funksiyalardan, generator funksiyalarından, asinxron funksiyalardan, metodlardan və s.

Yeni stil dərsləri

Bunu nəzərə alaraq, ES6 sinif sintaksisini araşdırmağın vaxtı gəldi. Əvvəlki kod nümunəsi birbaşa yeni sintaksisə aşağıdakı kimi çevrilir:

Əvvəlki kimi, hər bir sinif bir-birinə prototip və konstruktor xassələri ilə müraciət edən bir konstruktor funksiyası və bir prototip obyektindən ibarətdir. Bununla birlikdə, ikisinin tərif sırası geri çevrilir. Köhnə stil sinfi ilə konstruktor funksiyasını təyin edirsiniz və prototip obyekt sizin üçün yaradılır. Yeni bir stil sinfi ilə sinif tərifinin gövdəsi prototip obyektinin məzmununa çevrilir (statik metodlar xaricində) və bunların altında bir konstruktor təyin edirsiniz. Son nəticə hər iki halda da eynidır.

Beləliklə, ES6 sinif sintaksisi köhnə stil "sinifləri" üçün sadəcə şəkərdirsə, bu nə ilə əlaqədardır? Yeni sinif sintaksisi yalnız daha yaxşı görünür və təhlükəsizlik yoxlamaları əlavə etmir, eyni zamanda ES6-ya qədər mümkün olmayan, xüsusən sinif əsaslı miras funksiyaları təklif edir. Yeni sintaksis ilə bir sinif təyin edərkən, isteğe bağlı olaraq aşağıda göstərildiyi kimi, sinifin miras aldığı bir super sinif təmin edə bilərsiniz:

Lazımi kod daha çirkin olmasına baxmayaraq, bu nümunənin özü hələ heç bir sinif sintaksisiyle təqlid edilə bilər.

Sinifə əsaslanan miras ilə qayda sadədir: cütlüyün hər bir hissəsi prototip olaraq superklassın müvafiq hissəsinə malikdir. Beləliklə, super sinif qurucusu alt sinif konstruktorunun prototipidir və super sinif prototip obyekti alt sinif prototip obyektinin prototipidir. Burada illüstrasiya məqsədləri üçün bir diaqram var (yalnız [[prototiplər]] göstərilir; aydınlıq üçün xüsusiyyətlər buraxılmışdır).

Bu [[Prototip]] əlaqələrini sinif sintaksisiz qurmağın birbaşa və əlverişli yolu yoxdur. Bununla birlikdə, ES5-də təqdim olunan Object.setPrototypeOf () istifadə edərək onları əl ilə qura bilərsiniz.

Bununla birlikdə, yuxarıdakı nümunə konstruktorlarda xüsusi bir şey etməkdən çəkinir. Xüsusilə, super, alt siniflərin super sinifin xüsusiyyətlərinə və qurucusuna çata biləcəyi yeni bir sintaksisdən qaçınır. Bu, daha mürəkkəbdir və əslində ES5-də tam şəkildə təqlid edilə bilməz, baxmayaraq ki, ES6-da sinif sintaksisisiz və ya Reflect vasitəsilə təqlid edilə bilər.

Birinci sinif daşınmaz əmlaka giriş

Bir super sinif qurucusunu çağırmağın və ya super sinifin xüsusiyyətlərinə çatmağın iki yolu var. İkinci hal daha sadədir, buna görə əvvəlcə əhatə edəcəyik.

Mükəmməl işləmə yolu hər bir funksiyanın [[HomeObject]] adlı daxili bir yuvaya sahib olmasıdır ki, funksiyanın əvvəlcə bir metod olaraq təyin edildiyi zaman funksiyanın əvvəlcə təyin olunduğu obyektdir. Sinif tərifində bu obyekt sinifin prototip obyektidir; H. Foo.prototip. Bir əmlaka super.foo və ya super ["foo"] vasitəsi ilə daxil olsanız, [[HomeObject]] ilə bərabərdir. [[Prototip]]. Foo.

Super-nın pərdə arxasında necə işlədiyini anlamaqla, mürəkkəb və qeyri-adi şəraitdə belə necə davranacağını proqnozlaşdırmaq olar. Məsələn, bir funksiyanın [[HomeObject]] tərif vaxtında qurulur və sonradan funksiyanı aşağıda göstərildiyi kimi digər obyektlərə qaytarsanız da dəyişmir.

Yuxarıdakı nümunədə əvvəlcə D.prototipdə müəyyən edilmiş bir funksiyanı götürdük və B.prototipə köçürdük. HomeObject hələ də D.prototipi göstərdiyindən, super giriş C.prototip olan D.prototipin prototipinə baxır. Nəticə budur ki, C-nin fo-nun nüsxəsi, b-nin prototip zəncirində heç bir yerdə olmasa da, adlanır.

Eynilə, [[HomeObject]]. [[Prototip]] hər dəfə superexpression qiymətləndirildikdə, [[Prototip]] -də dəyişikliklər edilir və yeni nəticələr aşağıda göstərildiyi kimi qaytarılır.

Bir əlavə qeyd olaraq, Super sinif tərifləri ilə məhdudlaşmır. Bundan əlavə, yeni metod qısa forma ilə hərfi bir obyektdə təyin olunan hər hansı bir funksiya tərəfindən istifadə edilə bilər. Bu vəziyyətdə, [[HomeObject]] hərf edən bir obyektdir. Əlbətdə ki, bir cisim hərfinin [[prototipi]] həmişə "Object.prototype" dır, bu səbəbdən prototipi aşağıda göstərildiyi kimi əl ilə təyin etmədiyiniz təqdirdə bu xüsusilə faydalı deyil.

Super xassələri təqlid edin

[[HomeObject]] metodlarını əl ilə qurmağın bir yolu yoxdur, ancaq aşağıda göstərildiyi kimi yalnız dəyəri saxlayaraq və qətnaməni əl ilə edərək təqlid edə bilərik. Mükəmməl yazmaq qədər rahat deyil, amma heç olmasa işləyir.

Super metodun düzgün dəyərlə çağırıldığından əmin olmaq üçün .call (bu) istifadə etməliyik. Metodun nədənsə Function.prototype.call-ı kölgədə qoyan bir xüsusiyyəti varsa, bunun əvəzinə daha etibarlı, lakin daha açıq olan Function.prototype.call.call (foo, this) və ya Reflect.apply (foo, this) istifadə edə bilərik. .

Statik metodlarda əladır

Super statik metodlardan da istifadə edə bilərsiniz. Statik metodlar adi metodlarla eynidır, yalnız prototip obyekt yerinə konstruktor funksiyasındakı xüsusiyyətlər kimi təyin olunur.

super, normal metodlarla olduğu kimi statik metodlar daxilində də təqlid edilə bilər. Yeganə fərq budur ki, [[HomeObject]] indi prototip obyekt deyil, konstruktor funksiyasına çevrilmişdir.

Mükəmməl dizaynerlər

Normal konstruktor funksiyasının [[Construct]] metodu çağırıldıqda, yeni bir obyekt gizli şəkildə yaradılır və funksiyada bu dəyərə bağlanır. Bununla birlikdə, alt sinif inşaatçıları fərqli qaydalara əməl edirlər. Bu dəyər avtomatik olaraq yaradılmır və ona daxil olmaq cəhdi səhvlə nəticələnəcəkdir. Bunun əvəzinə super sinif qurucusunu super (args) vasitəsi ilə çağırmalısınız. Daha sonra super sinif qurucusunun nəticəsi bu dəyərin yerli dəyərinə bağlanır. Daha sonra alt sinif konstruktorunda hər zamanki kimi əldə edə bilərsiniz.

Əlbəttə ki, yeni stil sinifləri ilə düzgün işləyə biləcək köhnə bir stil sinfi yaratmaq istəyirsinizsə, bu problemlidir. Hər iki halda da əsas sinif konstruktoru adi bir konstruktor funksiyası olduğundan köhnə üslub sinfinin yeni üslub sinfinə tabe olması problem deyil. Lakin köhnə üslub konstruktorları hər zaman əsas konstruktor olduqları və alt sinif konstruktorları üçün xüsusi bir davranışı olmadığı üçün köhnə üslub sinfi ilə yeni bir stil sinifinin alt sinifləşdirilməsi düzgün işləmir.

Tutaq ki, tərifi bilinməyən və dəyişdirilə bilməyən yeni bir üslub sinfi Base-yə sahibik və onu əsl alt sinif gözləyən Base-dəki kodla uyğun olaraq, sinif sintaksisiz bölmək istəyirik.

Əvvəlcə, bazanın proksi və ya qeyri-müəyyən hesablanmış xüsusiyyətlərdən və ya başqa bir qeyri-adi bir şeydən istifadə etmədiyini düşünək, çünki həll yolumuz Base'nin xüsusiyyətlərinə həqiqi bir alt sinifdən fərqli olaraq və ya fərqli bir qaydada daxil olacaq və bununla bağlı heç bir şey edə bilmərik.

Sonra konstruktorun zəng zəncirinin necə qurulduğu sual yaranır. Normal super xassələrdə olduğu kimi, sadəcə Object.getPrototypeOf (homeObject) .constructor ilə super sinif konstruktorunu əldə edə bilərik. Bəs onu necə çağırmaq olar? Xoşbəxtlikdən, bir konstruktor funksiyasının daxili [[Construct]] metodunu əl ilə çağırmaq üçün Reflect.construct () istifadə edə bilərik.

Bu bağlamanın spesifik davranışını təqlid etməyin yolu yoxdur. Ancaq bunu görməməzlikdən gələ bilərik və aşağıdakı nümunədə "$ this" adlı "real" dəyəri saxlamaq üçün yerli bir dəyişən istifadə edə bilərik.

$ Bu qayıdışına diqqət yetirin; Yuxarıdakı xətt. Unutmayın ki, bir konstruktor funksiyası bir obyekti qaytardıqda, həmin obyekt örtüklə yaradılmış dəyər əvəzinə yeni ifadənin dəyəri kimi istifadə olunur.

Missiya yerinə yetirildi? Tam olaraq deyil. Yuxarıdakı nümunədəki obj dəyəri əslində Child nümunəsi deyil; H. Prototip zəncirində Uşaq.prototipi yoxdur. Base-nin qurucusunun Child haqqında heç bir şey bilməməsi, buna görə Base-in sadə bir nümunəsi olan bir obyekti geri qaytardığı (onun [[Prototip]] Base.prototype) olmasıdır.

Bu problem həqiqi siniflər üçün necə həll olunur? [[Construct]] və beləliklə Reflect.construct həqiqətən üç parametr götürür. Üçüncü parametr, newTarget, əvvəlcə yeni ifadədə çağırılan konstruktora və beləliklə miras hiyerarşisindəki ən aşağı (ən çox əldə edilən) sinifin qurucusuna istinad edir. İdarəetmə axını baza sinfinin konstruktoruna çatan kimi, örtüklə yaradılan obyekt prototip olaraq newTarget-a sahibdir.

Bu səbəbdən, Base konstruksiyasını konstruktoru Reflect.construct (constructor, args, Child) vasitəsi ilə çağıraraq Child nümunəsi edə bilərik. Bununla birlikdə, bu hələ də tamamilə doğru deyil, çünki başqası uşağını təqdim etdikdə həmişə kəsilir. Uşaq sinfini sərt kodlamaq əvəzinə, dəyişməz olaraq newTarget-a keçməliyik. Xoşbəxtlikdən, onlara xüsusi new.target sintaksisindən istifadə edərək konstruktorlardan daxil olmaq mümkündür. Bu, aşağıdakı son həllinə gətirib çıxarır:

Son toxunuş

Bu, siniflərin bütün əsas funksiyalarını əhatə edir, lakin bir neçə kiçik fərq var, əsasən yeni sinif sintaksisinə əlavə edilmiş təhlükəsizlik yoxlamaları. Məsələn, funksiya təriflərinə avtomatik olaraq əlavə edilən prototip xassəsi, varsayılan olaraq yazılabilir, lakin sinif qurucularının prototip xassəsi yazıla bilməz. Object.defineProperty () zəng etməklə asanlıqla özümüzün yazılmayan hala gətirə bilirik. Alternativ olaraq, hər şeyin dəyişməz olmasını istəyirsinizsə, Object.freeze () -ə də zəng edə bilərsiniz.

Başqa bir yeni qoruma, sinif konstruktorlarının yeni ilə qurmaq əvəzinə onlara zəng etməyə çalışarsanız bir TypeError atacağıdır. Yuxarıdakı konstruktorumuz bir TypeError da atır, ancaq dolayı yolla, çünki əgər funksiya [[Çağırış]] ed edilsə new.target təyin olunmur və Reflect.construct () son dəlil kimi təyin olunmamış şəkildə keçdiyiniz təqdirdə TypeError atır. Burada TypeError təsadüfi olduğundan, ortaya çıxan səhv mesajı olduqca qarışıqdır. Daha faydalı bir səhv mesajı ilə səhv atan new.target üçün açıq bir yoxlama əlavə etmək faydalı ola bilər.

Hər halda, ümid edirəm ki, bu yazı xoşunuza gəldi və tədqiq edərkən mənim qədər öyrəndim. Yuxarıda göstərilən üsullar real dünya kodeksində nadir hallarda faydalıdır, lakin qara sehrə, daha doğrusu, çatmağı tələb edən qeyri-adi bir istifadə vəziyyətiniz olduqda şeylərin başlıq altında necə işlədiyini anlamaq hələ də vacibdir. ilişib qaldıqda başqasının qara sehrini düzəldirsən.

PS Mənim kimi, ekranın altındakı Medium’un qeyd olunmağınızı istəyən və ya veb saytların ümumi sürücüsündə məzmunu oxumağı mümkün qədər çətin və zəhlətökən etmək üçün görünən nəhəng görünməyən bannerindən bezdiyiniz təqdirdə, Yalnız Kill Sticky tövsiyə edə bilərəm. Əlfəcin əlavə edə biləcəyiniz və səhifədəki "yapışqan" elementləri siləcək sadə bir javascript parçasıdır. Sadə səslənir, amma Kill Sticky ilə sörf etmək həyat dəyişir. Yalnız bir əlfəcin olduğundan, vacib səhifə elementlərini bir uBlock filtrində olduğu kimi təsadüfən silməkdən narahat olmayın. Ən pis halda, səhifəni yalnız yeniləyə bilərsiniz.