“Class-field-proposal” atau “Apa yang salah di tc39 commit”

Kita semua sudah lama menginginkan enkapsulasi normal dalam JS, yang dapat digunakan tanpa gerakan yang tidak perlu. Kami juga menginginkan konstruksi yang mudah untuk mendeklarasikan properti kelas. Dan akhirnya, kami ingin semua fitur dalam bahasa ini muncul sedemikian rupa agar tidak merusak aplikasi yang ada.


Kelihatannya di sini adalah kebahagiaan: kelas-bidang-proposal , yang setelah bertahun-tahun siksaan komite tc39 masih sampai ke stage 3 dan bahkan mendapatkan implementasi di chrome .


Jujur, saya benar-benar ingin menulis artikel tentang mengapa Anda harus menggunakan fitur bahasa baru dan bagaimana melakukannya, tetapi, sayangnya, artikel itu tidak akan membahas hal itu sama sekali.


Deskripsi saat ini hilang


Saya tidak akan mengulangi deskripsi asli , FAQ , dan perubahan dalam spesifikasi di sini , tetapi hanya menjelaskan secara singkat poin-poin utama.


Bidang kelas


Mendeklarasikan bidang dan menggunakannya di dalam kelas:


 class A { x = 1; method() { console.log(this.x); } } 

Akses ke bidang di luar kelas:


 const a = new A(); console.log(ax); 

Semuanya tampak jelas dan selama bertahun-tahun kami telah menggunakan sintaks ini menggunakan Babel dan TypeScript .


Hanya ada nuansa. Sintaks baru ini menggunakan [[Define]] , dan bukan [[Set]] semantik yang dengannya kita hidup selama ini.


Dalam praktiknya, ini berarti bahwa kode di atas tidak sama dengan ini:


 class A { constructor() { this.x = 1; } method() { console.log(this.x); } } 

Tetapi sebenarnya ini setara dengan ini:


 class A { constructor() { Object.defineProperty(this, "x", { configurable: true, enumerable: true, writable: true, value: 1 }); } method() { console.log(this.x); } } 

Dan, meskipun untuk contoh di atas, kedua pendekatan pada dasarnya melakukan hal yang sama, ini adalah perbedaan SANGAT SERIUS , dan inilah alasannya:


Katakanlah kita memiliki kelas induk seperti ini:


 class A { x = 1; method() { console.log(this.x); } } 

Berdasarkan itu, kami membuat yang lain:


 class B extends A { x = 2; } 

Dan mereka menggunakannya:


 const b = new B(); b.method(); //   2   

Kemudian, untuk beberapa alasan, kelas A diubah dengan cara yang tampaknya terbelakang:


 class A { _x = 1; //  ,   ,        get x() { return this._x; }; set x(val) { return this._x = val; }; method() { console.log(this._x); } } 

Dan untuk semantik [[Set]] , ini benar-benar perubahan yang kompatibel ke belakang, tetapi tidak untuk [[Define]] . Sekarang panggilan ke b.method() akan ditampilkan ke konsol 1 bukan 2 . Dan ini akan terjadi karena Object.defineProperty mendefinisikan ulang deskriptor properti dan, karenanya, pengambil / penyetel dari kelas A tidak akan dipanggil. Bahkan, di kelas anak, kami mengaburkan properti x dari orang tua, mirip dengan bagaimana kita bisa melakukan ini dalam lingkup leksikal:


 const x = 1; { const x = 2; } 

Benar, dalam hal ini, linter dengan aturan no-shadowed-variable / no-shadow akan menyelamatkan kita, tetapi kemungkinan seseorang akan membuat no-shadowed-class-field cenderung nol.


Ngomong-ngomong, saya akan berterima kasih untuk istilah Rusia yang lebih sukses karena shadowed .

Terlepas dari semua hal di atas, saya bukan lawan semantik baru yang tidak dapat diterima (walaupun saya lebih suka yang lain), karena memiliki aspek positifnya sendiri. Tapi, sayangnya, nilai tambah ini tidak melebihi minus yang paling penting - kami telah menggunakan semantik [[Set]] selama bertahun-tahun, karena ini digunakan dalam babel6 dan TypeScript secara default.


Benar, perlu dicatat bahwa dalam babel7 default telah diubah .

Lebih banyak diskusi orisinal tentang topik ini dapat dibaca di sini dan di sini .


Bidang pribadi


Dan sekarang kita akan beralih ke bagian paling kontroversial dari yang satu ini. Sangat kontroversial sehingga:


  1. Terlepas dari kenyataan bahwa itu sudah diterapkan di Chrome Canary dan bidang publik sudah diaktifkan secara default, bidang pribadi masih di belakang bendera;
  2. Terlepas dari kenyataan bahwa prozal awal untuk bidang pribadi digabungkan dengan yang saat ini, permintaan masih dibuat untuk pemisahan kedua fitur ini (misalnya, satu , dua , tiga dan empat );
  3. bahkan beberapa anggota komite (seperti Allen Wirfs-Brock dan Kevin Smith ) berbicara dan menawarkan alternatif , meskipun ada stage3 ;
  4. ini melewatkan satu set rekor untuk jumlah masalah - 129 di repositori saat ini + 96 di aslinya , versus 126 untuk BigInt , dan pemegang catatan sebagian besar komentar negatif ;
  5. Saya harus membuat utas terpisah dengan upaya untuk merangkum semua klaim yang menentangnya;
  6. Saya harus menulis FAQ terpisah yang mencakup bagian ini
    Namun, karena argumen yang agak lemah, diskusi seperti itu muncul ( satu , dua )
  7. Saya, secara pribadi, menghabiskan semua waktu luang saya (dan kadang-kadang bekerja) untuk jangka waktu yang lama untuk mencari tahu segalanya dan bahkan menemukan penjelasan mengapa dia seperti itu atau menawarkan alternatif yang cocok ;
  8. pada akhirnya, saya memutuskan untuk menulis artikel ulasan ini.

Bidang pribadi dinyatakan sebagai berikut:


 class A { #priv; } 

Dan akses ke mereka adalah sebagai berikut:


 class A { #priv = 1; method() { console.log(this.#priv); } } 

Saya bahkan tidak akan mengangkat topik bahwa model mental di balik ini tidak terlalu intuitif ( this.#priv !== this['#priv'] ), tidak menggunakan kata-kata private / protected sudah dipesan (yang tentunya akan menyebabkan rasa sakit tambahan untuk pengembang TypeScript), tidak jelas bagaimana memperluasnya untuk pengubah akses lainnya , dan sintaksisnya sendiri tidak terlalu indah. Meskipun semua ini adalah alasan asli yang mendorong saya untuk belajar lebih dalam dan berpartisipasi dalam diskusi.


Ini semua berhubungan dengan sintaksis, di mana preferensi estetika subyektif sangat kuat. Dan seseorang dapat hidup dengannya dan terbiasa dengannya seiring waktu. Jika bukan karena satu hal: ada masalah semantik yang sangat signifikan ...


Semantics WeakMap


Mari kita lihat apa yang ada di balik proposisi yang ada. Kita dapat menulis ulang contoh di atas dengan enkapsulasi dan tanpa menggunakan sintaks baru, tetapi mempertahankan semantik dari yang sekarang:


 const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } } 

Ngomong-ngomong, berdasarkan semantik ini, salah satu anggota komite bahkan membangun perpustakaan utilitas kecil yang memungkinkan Anda untuk menggunakan negara swasta sekarang, untuk menunjukkan bahwa fungsi seperti itu terlalu dibesar-besarkan oleh komite. Kode yang diformat hanya membutuhkan 27 baris.

Secara umum, semuanya cukup baik, kita mendapatkan hard-private , yang tidak dapat diperoleh / dicegat / dilacak dengan cara apa pun dari kode eksternal, dan pada saat yang sama kita dapat mengakses bidang pribadi dari instance lain dari kelas yang sama, misalnya seperti ini:


 isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; } 

Yah, ini sangat mudah, kecuali untuk fakta bahwa semantik ini, selain enkapsulasi itu sendiri, juga mencakup brand-checking (Anda tidak dapat mengetahui apa itu Google - Anda tidak mungkin menemukan informasi yang relevan).
brand-checking adalah kebalikan dari duck-typing , dalam arti bahwa itu tidak memeriksa antarmuka publik objek, tetapi fakta bahwa objek itu dibangun menggunakan kode tepercaya.
Cek semacam itu, pada kenyataannya, memiliki cakupan tertentu - ini terutama terkait dengan keamanan memanggil kode yang tidak dipercaya dalam ruang alamat tunggal dengan yang dipercaya dan kemampuan untuk bertukar objek secara langsung tanpa serialisasi.


Meskipun beberapa insinyur menganggap ini bagian penting dari enkapsulasi yang tepat.

Terlepas dari kenyataan bahwa ini adalah kesempatan yang agak aneh, yang terkait erat dengan pola (deskripsi pendek dan lebih panjang ), propaganda Realms dan karya ilmiah di bidang Ilmu Komputer, di mana Mark Samuel Miller (dia juga anggota komite) terlibat, dalam pengalaman saya , dalam praktik kebanyakan pengembang, ini hampir tidak pernah terjadi.


Kebetulan, saya masih menemukan membran (meskipun saya tidak tahu apa itu) ketika saya menulis ulang vm2 agar sesuai dengan kebutuhan saya.

Masalah brand-checking


Seperti disebutkan sebelumnya, brand-checking adalah kebalikan dari duck-typing . Dalam praktiknya, ini berarti memiliki kode ini:


 const brands = new WeakMap(); class A { constructor() { brands.set(this, {}); } method() { return 1; } brandCheckedMethod() { if (!brands.has(this)) throw 'Brand-check failed'; console.log(this.method()); } } 

brandCheckedMethod hanya bisa dipanggil dengan instance dari kelas A dan bahkan jika target adalah objek yang mempertahankan invarian dari kelas ini, metode ini akan mengeluarkan pengecualian:


 const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method(); //        1 duckTypedObj.brandCheckedMethod(); //      

Jelas, contoh ini cukup sintetik dan penggunaan duckTypedObj seperti ini diragukan, sampai kita berpikir tentang Proxy .
Salah satu skenario penggunaan proxy yang sangat penting adalah metaprogramming. Agar proxy dapat melakukan semua pekerjaan bermanfaat yang diperlukan, metode objek yang dibungkus menggunakan proxy harus dieksekusi dalam konteks proxy, dan bukan dalam konteks target, yaitu:


 const a = new A(); const proxy = new Proxy(a, { get(target, p, receiver) { const property = Reflect.get(target, p, receiver); doSomethingUseful('get', retval, target, p, receiver); return (typeof property === 'function') ? property.bind(proxy) : property; } }); 

Panggil proxy.method(); akan melakukan pekerjaan yang bermanfaat yang dinyatakan dalam proxy dan mengembalikan 1 , sambil memanggil proxy.brandCheckedMethod(); alih-alih melakukan pekerjaan yang bermanfaat dua kali dari proksi, itu akan mengeluarkan pengecualian, karena a !== proxy , yang berarti brand-check tidak lulus.


Ya, kami dapat menjalankan metode / fungsi dalam konteks target nyata, bukan proxy, dan untuk beberapa skenario ini cukup (misalnya, untuk menerapkan pola ), tetapi ini tidak cukup untuk semua kasus (misalnya, untuk menerapkan properti reaktif: MobX 5 sudah menggunakan proxy untuk ini, Vue.js dan Aurelia sedang bereksperimen dengan pendekatan ini untuk rilis di masa mendatang).


Secara umum, selama brand-check perlu dilakukan secara eksplisit, ini bukan masalah - pengembang hanya perlu memutuskan trade-off yang dibuatnya dan apakah ia membutuhkannya, apalagi, dalam hal brand-check secara eksplisit brand-check Anda dapat menerapkannya sedemikian rupa sehingga kesalahan tidak akan terjadi pada proksi tepercaya.


Sayangnya, yang sekarang telah merampas fleksibilitas kami:


 class A { #priv; method() { this.#priv; //    brand-check   } } 

method seperti itu akan selalu melemparkan pengecualian jika tidak dipanggil dalam konteks objek yang dibangun menggunakan konstruktor A Dan bagian terburuknya adalah brand-check tersirat di sini dan dicampur dengan fungsi lain - enkapsulasi.


Meskipun hampir diperlukan untuk kode apa pun, brand-check memiliki cakupan yang agak sempit. Dan menggabungkannya menjadi satu sintaks akan mengarah pada fakta bahwa banyak brand-check tidak disengaja muncul dalam kode pengguna, ketika pengembang hanya bermaksud menyembunyikan detail implementasi.
Dan slogan yang digunakan untuk mempromosikan ini adalah # is the new _ hanya memperburuk situasi.


Anda juga dapat membaca diskusi terperinci tentang bagaimana prozal yang ada memecah proxy . Salah satu pengembang dan penulis Aurelia Vue.js berbicara dalam diskusi .

Juga, komentar saya , yang menjelaskan secara lebih rinci perbedaan antara skenario proksi yang berbeda, mungkin tampak menarik bagi seseorang. Secara keseluruhan, seluruh diskusi tentang koneksi bidang pribadi dan membran .

Alternatif


Semua diskusi ini tidak masuk akal jika tidak ada alternatif. Sayangnya, tidak ada satu pun pengganti yang ada di stage1 , dan, sebagai hasilnya, bahkan tidak memiliki kesempatan untuk dikerjakan cukup. Namun, saya akan mendaftar di sini alternatif yang entah bagaimana menyelesaikan masalah yang dijelaskan di atas.


  1. Symbol.private - prozazil alternatif salah satu anggota komite.
    1. Ini menyelesaikan semua masalah di atas (walaupun mungkin memiliki masalahnya sendiri, tetapi, mengingat kurangnya pekerjaan aktif di sana, sulit untuk menemukannya)
    2. sekali lagi dilemparkan kembali pada pertemuan terakhir komite karena kurangnya pemeriksaan brand-check terintegrasi, masalah dengan pola membran (meskipun ini + ini menawarkan solusi yang memadai) dan kurangnya sintaksis yang mudah digunakan
    3. Sintaks yang mudah digunakan dapat dibangun di atas yang sebenarnya, seperti yang saya tunjukkan di sini dan di sini
  2. Kelas 1.1 - posozal sebelumnya dari penulis yang sama
  3. Menggunakan pribadi sebagai objek

Alih-alih sebuah kesimpulan


Dengan nada artikel itu, mungkin kelihatannya saya mengutuk komite - ini tidak benar. Menurut saya, selama bertahun-tahun (tergantung pada apa titik awalnya, bahkan mungkin beberapa dekade) bahwa komite bekerja pada enkapsulasi di JS, banyak hal dalam industri telah berubah, dan tampilan dapat dikaburkan, yang menyebabkan peringkat prioritas yang salah .


Selain itu, kami, sebagai komunitas, mendorong tc39 yang memaksa mereka untuk merilis fitur lebih cepat, sementara kami memberikan umpan balik yang sangat sedikit di tahap awal prozos, menurunkan kemarahan kami hanya pada saat ketika sedikit yang dapat diubah.


Diyakini bahwa dalam kasus ini, prosesnya gagal.


Setelah memasukkannya ke dalam kepala saya dan berbicara dengan beberapa perwakilan, saya memutuskan bahwa saya akan melakukan yang terbaik untuk mencegah terulangnya situasi yang sama - tetapi saya dapat melakukan sedikit (menulis artikel ulasan, melakukan implementasi stage1 hilang dalam babel dan itu saja).


Tetapi yang paling penting adalah umpan balik - jadi saya akan meminta Anda untuk mengambil bagian dalam survei kecil ini. Dan saya, pada gilirannya, akan mencoba menyampaikannya kepada komite.

Source: https://habr.com/ru/post/id428119/


All Articles