Banyak artikel bagus telah ditulis tentang model objek dalam JavaScript. Dan tentang berbagai cara untuk membuat anggota kelas privat di Internet penuh dengan deskripsi yang layak. Tetapi tentang metode yang dilindungi - hanya ada sedikit data. Saya ingin mengisi celah ini dan memberi tahu bagaimana Anda dapat membuat metode yang dilindungi tanpa pustaka di JavaScript ECMAScript 5 murni.
Dalam artikel ini:
Tautan ke repositori git-hub dengan kode sumber dan tes. Mengapa anggota kelas yang dilindungi diperlukan
Singkatnya, kalau begitu
- lebih mudah untuk memahami operasi kelas dan menemukan kesalahan di dalamnya. (Anda dapat segera melihat dalam kasus mana anggota kelas digunakan. Jika pribadi, maka Anda hanya perlu menganalisis kelas ini, baik, dan jika dilindungi, maka hanya kelas ini dan yang diturunkan.)
- lebih mudah mengelola perubahan. (Misalnya, Anda dapat menghapus anggota pribadi tanpa takut akan terjadi sesuatu di luar kelas yang dapat diedit.)
- jumlah aplikasi dalam pelacak bug berkurang, karena pengguna perpustakaan atau kontrol dapat "menjahit" anggota "pribadi" kami, yang kami putuskan untuk hapus dalam versi baru kelas, atau mengubah logika pekerjaan mereka.
- Dan secara umum, anggota kelas yang dilindungi adalah alat desain. Sangat baik untuk membuatnya berguna dan teruji dengan baik.
Biarkan saya mengingatkan Anda bahwa ide utama anggota yang dilindungi adalah untuk menyembunyikan metode dan properti dari pengguna instance kelas, tetapi pada saat yang sama memungkinkan kelas turunan memiliki akses ke mereka.
Menggunakan TypeScript tidak akan memungkinkan pemanggilan metode yang dilindungi, setelah kompilasi dalam JavaScript, semua anggota pribadi dan yang dilindungi menjadi publik. Misalnya, kami mengembangkan kontrol atau pustaka yang akan dipasang pengguna di situs atau aplikasi mereka. Para pengguna ini akan dapat melakukan apa pun yang mereka inginkan dengan anggota yang dilindungi, melanggar integritas kelas. Akibatnya, pelacak bug kami penuh dengan keluhan bahwa perpustakaan atau kontrol kami tidak berfungsi dengan baik. Kami menghabiskan waktu dan upaya untuk mengatasinya -
"Apakah ini objeknya dalam keadaan klien, yang menyebabkan kesalahan?!" . Oleh karena itu, untuk membuat hidup lebih mudah bagi semua orang, perlindungan semacam itu diperlukan yang tidak akan memungkinkan untuk mengubah makna anggota kelas yang privat dan terlindungi.
Apa yang dibutuhkan untuk memahami metode yang dimaksud
Untuk memahami metode mendeklarasikan anggota kelas yang dilindungi, Anda membutuhkan pengetahuan yang kuat:
- kelas dan objek perangkat dalam JavaScript.
- cara membuat anggota kelas privat (setidaknya melalui penutupan).
- metode Object.defineProperty dan Object.getOwnPropertyDescriptor
Tentang model perangkat dalam JavaScript, saya dapat merekomendasikan, misalnya, artikel yang sangat baik oleh Andrey Akinshin (
DreamWalker )
"Memahami OOP di JS [Bagian No. 1]" .
Tentang properti pribadi ada yang bagus dan, menurut saya, deskripsi yang cukup lengkap tentang
sebanyak 4 cara berbeda untuk membuat anggota kelas privat di situs web MDN.
Adapun metode Object.defineProperty, itu akan memungkinkan kita untuk menyembunyikan properti dan metode dari for-in loop, dan, sebagai akibatnya, dari algoritma serialisasi:
function MyClass(){ Object.defineProperty(MyClass.prototype, 'protectedNumber', { value: 12, enumerable: false }); this.publicNumber = 25; }; var obj1 = new MyClass(); for(var prop in obj1){ console.log('property:' prop);
Penyembunyian seperti itu harus dilakukan, tetapi ini, tentu saja, tidak cukup karena Masih ada kemungkinan untuk memanggil metode / properti secara langsung:
console.log(obj1.protectedNumber);
Helper Class ProtectedError
Pertama, kita membutuhkan kelas ProtectedError, yang mewarisi dari Kesalahan, dan yang akan dilempar jika tidak ada akses ke metode atau properti yang dilindungi.
function ProtectedError(){ this.message = "Encapsulation error, the object member you are trying to address is protected."; } ProtectedError.prototype = new Error(); ProtectedError.prototype.constructor = ProtectedError;
Menerapkan anggota kelas yang dilindungi di ES5
Sekarang kita memiliki kelas ProtectedError dan kita mengerti apa yang Object.defineProperty lakukan dengan nilai yang dapat dihitung: false, mari kita menganalisis penciptaan kelas dasar yang ingin berbagi metode protectedMethod dengan semua kelas turunannya, tetapi sembunyikan dari orang lain:
function BaseClass(){ if (!(this instanceof BaseClass)) return new BaseClass(); var _self = this;
Deskripsi konstruktor kelas BaseClass
Mungkin Anda akan bingung dengan cek:
if (!(this instanceof BaseClass)) return new BaseClass();
Tes ini adalah "amatir". Anda dapat menghapusnya, itu tidak ada hubungannya dengan metode yang dilindungi. Namun, saya pribadi membiarkannya di kode saya, karena itu diperlukan untuk kasus-kasus ketika instance kelas tidak dibuat dengan benar, yaitu tanpa kata kunci baru. Misalnya, seperti ini:
var obj1 = BaseClass();
Dalam kasus seperti itu, lakukan yang Anda inginkan. Anda dapat, misalnya, menghasilkan kesalahan:
if (!(this instanceof BaseClass)) throw new Error('Wrong instance creation. Maybe operator "new" was forgotten');
Atau Anda cukup instantiate dengan benar, seperti yang dilakukan pada BaseClass.
Selanjutnya, kita menyimpan instance baru dalam variabel _self (mengapa saya perlu menjelaskan ini nanti).
Deskripsi properti publik bernama protectedMethod
Memasuki metode ini, kami memanggil pemeriksaan konteks tempat kami dipanggil. Lebih baik untuk memeriksa dalam metode terpisah, misalnya, checkAccess, karena pemeriksaan yang sama akan diperlukan dalam semua metode dan properti kelas yang dilindungi. Jadi, pertama-tama, periksa jenis panggilan untuk konteks ini. Jika ini memiliki tipe selain BaseClass, maka jenisnya bukan BaseClass itu sendiri maupun turunannya. Kami melarang panggilan semacam itu.
if(!(this instanceof BaseClass)) throw new ProtectedError();
Bagaimana ini bisa terjadi? Misalnya, seperti ini:
var b = new BaseClass(); var someObject = {}; b.protectedMethod.call(someObject);
Dalam kasus kelas turunan, ekspresi instanceClassClass ini akan benar. Tetapi untuk instance BaseClass, ekspresi BaseClass instance ini akan benar. Oleh karena itu, untuk membedakan instance dari kelas BaseClass dari instance kelas turunan, kami memeriksa konstruktor. Jika konstruktor cocok dengan BaseClass, maka protectedMethod kami dipanggil pada instance BaseClass, seperti metode publik biasa:
var b = new BaseClass(); b.protectedMethod();
Kami melarang panggilan semacam itu:
if(this.constructor === BaseClass) throw new ProtectedError();
Selanjutnya adalah panggilan metode tertutup protectedMethod, yang, pada kenyataannya, adalah metode yang kami lindungi. Di dalam metode, jika Anda perlu merujuk ke anggota kelas BaseClass, Anda dapat melakukan ini menggunakan instance _self yang tersimpan. Inilah yang _self diciptakan untuk memiliki akses ke anggota kelas dari semua metode pribadi / pribadi. Oleh karena itu, jika Anda tidak perlu mengakses anggota kelas dalam metode atau properti yang dilindungi, maka Anda tidak dapat membuat variabel _self.
Memanggil metode yang dilindungi di dalam kelas BaseClass
Di dalam kelas BaseClass, protectedMethod harus diakses hanya dengan nama, bukan melalui ini. Kalau tidak, di dalam protectedMethod kita tidak bisa membedakan apakah kita dipanggil sebagai metode publik atau dari dalam suatu kelas. Dalam hal ini, penutupan menghemat kita - protectedMethod berperilaku seperti metode pribadi biasa, ditutup di dalam kelas dan hanya terlihat dalam lingkup fungsi BaseClass.
DerivedClass Derived Class Description
Sekarang mari kita lihat kelas turunan dan bagaimana membuatnya dapat diakses oleh metode yang dilindungi dari kelas dasar.
function DerivedClass(){ var _base = { protectedMethod: this.protectedMethod.bind(this) }; function checkAccess() { if (this.constructor === DerivedClass) throw new ProtectedError(); }
Deskripsi konstruktor kelas turunan
Di kelas turunan, kita membuat objek _base di mana kita menempatkan referensi ke metode protectedMethod dari kelas dasar, ditutup ke konteks kelas turunan melalui metode bind standar. Ini berarti memanggil _base.protectedMethod (); di dalam protectedMethod ini bukan objek _base, tetapi turunan dari kelas DerivedClass.
Deskripsi Metode ProtectedMethod Di dalam DerivedClass
Di kelas DerivedClass, perlu untuk mendeklarasikan metode publik protectedMethod dengan cara yang sama seperti yang kami lakukan di kelas dasar melalui Object.defineProperty dan memeriksa akses di dalamnya dengan memanggil metode checkAccess atau dengan memeriksa langsung dalam metode:
Object.defineProperty(DerivedClass.prototype, 'protectedMethod', { enumerable: false, configurable: false, value: function(){ if(this.constructor === DerivedClass) throw new ProtectedError() return _base.protectedMethod(); } });
Kami memeriksa -
"tetapi apakah kita telah dipanggil sebagai metode publik yang sederhana?" Untuk contoh kelas DerivedClass, konstruktor akan sama dengan DerivedClass. Jika demikian, maka hasilkan kesalahan. Kalau tidak, kami mengirimnya ke kelas dasar dan itu sudah akan melakukan semua pemeriksaan lainnya.
Jadi, di kelas turunan, kami memiliki dua fungsi. Satu dideklarasikan melalui Object.defineProperty dan diperlukan untuk kelas turunan DerivedClass. Ini publik dan karenanya memiliki cek yang melarang panggilan publik. Metode kedua terletak di objek _base, yang ditutup di dalam kelas DerivedClass dan karenanya tidak terlihat oleh siapa pun dari luar dan digunakan untuk mengakses metode yang dilindungi dari semua metode DerivedClass.
Perlindungan Properti
Dengan properti, pekerjaan terjadi sedikit berbeda. Properti di BaseClass didefinisikan seperti biasa melalui Object.defineProperty, hanya di getter dan setter Anda pertama-tama perlu menambahkan cek kami, mis. panggil checkAccess:
function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return _protectedProperty; }, set: function (value) { checkAccess.call(this); _protectedProperty = value; }, enumerable: false, configurable: false }); }
Di dalam kelas BaseClass, properti yang dilindungi diakses bukan melalui ini, tetapi ke variabel tertutup _protectedProperty. Jika penting bagi kita bahwa pengambil dan penyetel bekerja saat menggunakan properti di dalam kelas BaseClass, maka kita perlu membuat metode pribadi getProtectedPropety dan setProtectedProperty, di dalamnya tidak akan ada pemeriksaan, dan mereka seharusnya sudah dipanggil.
function BaseClass(){ function checkAccess(){ ... } var _protectedProperty; Object.defineProperty(this, 'protectedProperty', { get: function () { checkAccess.call(this); return getProtectedProperty(); }, set: function (value) { checkAccess.call(this); setProtectedProperty(value); }, enumerable: false, configurable: false }); function getProtectedProperty(){
Di kelas turunan, bekerja dengan properti sedikit lebih rumit, karena properti tidak dapat diganti oleh konteks. Oleh karena itu, kita akan menggunakan metode Object.getOwnPropertyDescriptor standar untuk mendapatkan pengambil dan penyetel dari properti kelas dasar sebagai fungsi yang sudah dapat mengubah konteks panggilan:
function DerivedClass(){ function checkAccess(){ ... } var _base = { protectedMethod: _self.protectedMethod.bind(_self), }; var _baseProtectedPropertyDescriptor = Object.getOwnPropertyDescriptor(_self, 'protectedProperty');
Deskripsi warisan
Dan hal terakhir yang ingin saya komentari adalah warisan DerivedClass dari BaseClass. Seperti yang Anda ketahui, DerivedClass.prototype = BaseClass baru (); tidak hanya membuat prototipe, tetapi juga menulis ulang properti konstruktornya. Karena itu, untuk setiap instance DerivedClass, properti konstruktor menjadi sama dengan BaseClass. Untuk memperbaikinya, biasanya setelah membuat prototipe, tulis ulang properti konstruktor:
DerivedClass.prototype = new BaseClass(); DerivedClass.prototype.constructor = DerivedClass;
Namun, agar tidak ada yang menulis ulang properti ini setelah kami, kami menggunakan Object.defineProperty yang sama. Properti yang dapat dikonfigurasi: salah mencegah properti ditimpa kembali:
DerivedClass.prototype = new BaseClass(); Object.defineProperty(DerivedClass.prototype, 'constructor', { value : DerivedClass, configurable: false });