Dalam pemrograman, kita sering kali ingin mengambil sesuatu dan mengembangkannya.
Misalnya, kita mempunyai objek user
dengan properti dan metodenya, dan ingin menjadikan admin
dan guest
sebagai varian yang sedikit dimodifikasi. Kami ingin menggunakan kembali apa yang kami miliki di user
, bukan menyalin/menerapkan kembali metodenya, hanya membuat objek baru di atasnya.
Warisan prototipe adalah fitur bahasa yang membantu dalam hal itu.
Dalam JavaScript, objek memiliki properti tersembunyi khusus [[Prototype]]
(seperti yang disebutkan dalam spesifikasi), yaitu null
atau mereferensikan objek lain. Objek itu disebut “prototipe”:
Saat kita membaca properti dari object
, dan properti tersebut hilang, JavaScript secara otomatis mengambilnya dari prototipe. Dalam pemrograman, ini disebut “warisan prototipe”. Dan segera kita akan mempelajari banyak contoh pewarisan tersebut, serta fitur bahasa keren yang dibangun di atasnya.
Properti [[Prototype]]
bersifat internal dan tersembunyi, tetapi ada banyak cara untuk mengaturnya.
Salah satunya adalah dengan menggunakan nama khusus __proto__
, seperti ini:
biarkan hewan = { makan: benar }; biarkan kelinci = { melompat: benar }; kelinci.__proto__ = binatang; // menyetel kelinci.[[Prototipe]] = hewan
Sekarang jika kita membaca properti dari rabbit
, dan properti itu hilang, JavaScript akan secara otomatis mengambilnya dari animal
.
Misalnya:
biarkan hewan = { makan: benar }; biarkan kelinci = { melompat: benar }; kelinci.__proto__ = binatang; // (*) // kita bisa menemukan kedua properti di kelinci sekarang: waspada( kelinci.makan ); // BENAR (**) alert( kelinci.jumps ); // BENAR
Di sini garis (*)
menetapkan animal
menjadi prototipe rabbit
.
Lalu, saat alert
mencoba membaca properti rabbit.eats
(**)
, properti tersebut tidak ada di rabbit
, jadi JavaScript mengikuti referensi [[Prototype]]
dan menemukannya di animal
(lihat dari bawah ke atas):
Di sini kita dapat mengatakan bahwa “ animal
adalah prototipe rabbit
” atau “prototip rabbit
mewarisi dari animal
”.
Jadi jika animal
mempunyai banyak sifat dan metode yang berguna, maka secara otomatis sifat dan metode tersebut tersedia pada rabbit
. Properti seperti itu disebut “diwariskan”.
Jika kita mempunyai metode di animal
, maka bisa dipanggil di rabbit
:
biarkan hewan = { makan: benar, berjalan() { alert("Hewan berjalan"); } }; biarkan kelinci = { melompat: benar, __proto__: binatang }; // walk diambil dari prototipe kelinci.berjalan(); // Jalan-jalan binatang
Metodenya otomatis diambil dari prototype, seperti ini:
Rantai prototipe bisa lebih panjang:
biarkan hewan = { makan: benar, berjalan() { alert("Hewan berjalan"); } }; biarkan kelinci = { melompat: benar, __proto__: binatang }; misalkan telinga panjang = { panjang telinga: 10, __proto__: kelinci }; // walk diambil dari rantai prototipe telinga panjang.berjalan(); // Jalan-jalan binatang alert(longEar.jumps); // benar (dari kelinci)
Sekarang jika kita membaca sesuatu dari longEar
, dan itu hilang, JavaScript akan mencarinya di rabbit
, dan kemudian di animal
.
Hanya ada dua batasan:
Referensi tidak boleh berputar-putar. JavaScript akan menimbulkan kesalahan jika kita mencoba menetapkan __proto__
dalam lingkaran.
Nilai __proto__
dapat berupa objek atau null
. Jenis lainnya diabaikan.
Mungkin juga sudah jelas, tapi tetap saja: hanya ada satu [[Prototype]]
. Suatu objek tidak boleh mewarisi dari dua objek lainnya.
__proto__
adalah pengambil/penyetel historis untuk [[Prototype]]
Merupakan kesalahan umum bagi pengembang pemula untuk tidak mengetahui perbedaan antara keduanya.
Harap dicatat bahwa __proto__
tidak sama dengan properti internal [[Prototype]]
. Ini adalah pengambil/penyetel untuk [[Prototype]]
. Nanti kita akan melihat situasi yang penting, untuk saat ini mari kita ingat saja, saat kita membangun pemahaman kita tentang bahasa JavaScript.
Properti __proto__
agak ketinggalan jaman. Itu ada karena alasan historis, JavaScript modern menyarankan agar kita menggunakan fungsi Object.getPrototypeOf/Object.setPrototypeOf
yang mendapatkan/mengatur prototipe. Kami juga akan membahas fungsi-fungsi ini nanti.
Berdasarkan spesifikasinya, __proto__
hanya boleh didukung oleh browser. Faktanya, semua lingkungan termasuk dukungan sisi server __proto__
, jadi kami cukup aman menggunakannya.
Karena notasi __proto__
sedikit lebih jelas secara intuitif, kami menggunakannya dalam contoh.
Prototipe hanya digunakan untuk membaca properti.
Operasi tulis/hapus bekerja langsung dengan objek.
Pada contoh di bawah ini, kami menetapkan metode walk
sendiri ke rabbit
:
biarkan hewan = { makan: benar, berjalan() { /* metode ini tidak akan digunakan oleh kelinci */ } }; biarkan kelinci = { __proto__: binatang }; kelinci.berjalan = fungsi() { alert("Kelinci! Pantulan-pantulan!"); }; kelinci.berjalan(); // Kelinci! Bouncing-bouncing!
Mulai sekarang, panggilan rabbit.walk()
akan segera menemukan metode di objek dan mengeksekusinya, tanpa menggunakan prototipe:
Properti pengakses merupakan pengecualian, karena penugasan ditangani oleh fungsi penyetel. Jadi menulis ke properti seperti itu sebenarnya sama dengan memanggil suatu fungsi.
Oleh karena itu admin.fullName
berfungsi dengan benar pada kode di bawah ini:
biarkan pengguna = { nama: "Yohanes", nama keluarga: "Smith", setel Nama Lengkap(nilai) { [nama.ini, nama belakang ini] = nilai.split(" "); }, dapatkan Nama Lengkap() { return `${nama ini} ${nama belakang ini}`; } }; biarkan admin = { __proto__: pengguna, isAdmin: benar }; alert(admin.Nama Lengkap); // John Smith (*) // pemicu penyetel! admin.fullName = "Alice Cooper"; // (**) alert(admin.Nama Lengkap); // Alice Cooper, status admin diubah alert(pengguna.Nama Lengkap); // John Smith, status dilindungi pengguna
Di sini, di baris (*)
properti admin.fullName
memiliki pengambil di prototipe user
, begitulah namanya. Dan pada baris (**)
properti tersebut memiliki penyetel di prototipe, demikianlah sebutannya.
Sebuah pertanyaan menarik mungkin muncul pada contoh di atas: berapa nilai di dalam set fullName(value)
this
? Di manakah properti this.name
dan this.surname
ditulis: menjadi user
atau admin
?
Jawabannya sederhana: this
tidak terpengaruh oleh prototipe sama sekali.
Tidak peduli di mana metode itu ditemukan: dalam suatu objek atau prototipenya. Dalam pemanggilan metode, this
selalu berupa objek sebelum titik.
Jadi, panggilan penyetel admin.fullName=
menggunakan admin
sebagai this
, bukan user
.
Itu sebenarnya adalah hal yang sangat penting, karena kita mungkin mempunyai objek besar dengan banyak metode, dan memiliki objek yang mewarisinya. Dan ketika objek yang diwarisi menjalankan metode yang diwariskan, mereka hanya akan mengubah statusnya sendiri, bukan status objek besarnya.
Misalnya, di sini animal
mewakili “metode penyimpanan”, dan rabbit
memanfaatkannya.
Panggilan rabbit.sleep()
menetapkan this.isSleeping
pada objek rabbit
:
// hewan punya metode biarkan hewan = { berjalan() { if (!ini.sedang Tidur) { alert(`Saya berjalan`); } }, tidur() { this.isSleeping = benar; } }; biarkan kelinci = { nama: "Kelinci Putih", __proto__: binatang }; // memodifikasi kelinci.isSleeping kelinci.tidur(); alert(kelinci.sedang tidur); // BENAR alert(animal.isSleeping); // tidak terdefinisi (tidak ada properti seperti itu di prototipe)
Gambar yang dihasilkan:
Jika kita memiliki objek lain, seperti bird
, snake
, dll., yang diwarisi dari animal
, mereka juga akan mendapatkan akses ke metode animal
. Tapi this
di setiap pemanggilan metode akan menjadi objek terkait, dievaluasi pada waktu panggilan (sebelum titik), bukan animal
. Jadi ketika kita menulis data ke dalam this
, data tersebut disimpan ke dalam objek-objek ini.
Akibatnya, metode dibagikan, namun status objek tidak.
Perulangan for..in
juga mengulangi properti yang diwarisi.
Misalnya:
biarkan hewan = { makan: benar }; biarkan kelinci = { melompat: benar, __proto__: binatang }; // Object.keys hanya mengembalikan kunci sendiri alert(Object.keys(kelinci)); // melompat // for..in mengulang kunci milik sendiri dan warisan for(biarkan prop masuk kelinci) alert(prop); // melompat, lalu makan
Jika bukan itu yang kita inginkan, dan kita ingin mengecualikan properti warisan, ada metode bawaan obj.hasOwnProperty(key): metode ini akan mengembalikan true
jika obj
memiliki propertinya sendiri (bukan warisan) bernama key
.
Jadi kita dapat memfilter properti yang diwariskan (atau melakukan hal lain dengannya):
biarkan hewan = { makan: benar }; biarkan kelinci = { melompat: benar, __proto__: binatang }; for(biarkan penyangga pada kelinci) { biarkan isOwn = kelinci.hasOwnProperty(prop); jika (adalah Milik Sendiri) { alert(`Kami: ${prop}`); // Kami: melompat } kalau tidak { alert(`Warisan: ${prop}`); // Warisan: makan } }
Di sini kita memiliki rantai pewarisan berikut: rabbit
mewarisi dari animal
, yang mewarisi dari Object.prototype
(karena animal
adalah objek literal {...}
, jadi secara default), dan kemudian null
di atasnya:
Perhatikan, ada satu hal yang lucu. Dari mana metode rabbit.hasOwnProperty
berasal? Kami tidak mendefinisikannya. Melihat rantainya kita dapat melihat bahwa metode ini disediakan oleh Object.prototype.hasOwnProperty
. Dengan kata lain, itu diwariskan.
…Tetapi mengapa hasOwnProperty
tidak muncul di loop for..in
seperti yang dilakukan eats
dan jumps
, jika for..in
mencantumkan properti yang diwarisi?
Jawabannya sederhana: tidak dapat dihitung. Sama seperti semua properti Object.prototype
lainnya, ia memiliki tanda enumerable:false
. Dan for..in
hanya mencantumkan properti yang dapat dihitung. Itu sebabnya properti ini dan properti Object.prototype
lainnya tidak terdaftar.
Hampir semua metode perolehan kunci/nilai lainnya mengabaikan properti yang diwariskan
Hampir semua metode perolehan kunci/nilai lainnya, seperti Object.keys
, Object.values
dan sebagainya mengabaikan properti yang diwariskan.
Mereka hanya beroperasi pada objek itu sendiri. Properti dari prototipe tidak diperhitungkan.
Dalam JavaScript, semua objek memiliki properti [[Prototype]]
tersembunyi yang berupa objek lain atau null
.
Kita dapat menggunakan obj.__proto__
untuk mengaksesnya (pengambil/penyetel riwayat, ada cara lain, yang akan segera dibahas).
Objek yang direferensikan oleh [[Prototype]]
disebut “prototipe”.
Jika kita ingin membaca properti obj
atau memanggil suatu metode, dan metode tersebut tidak ada, maka JavaScript akan mencoba menemukannya di prototipe.
Operasi tulis/hapus bertindak langsung pada objek, mereka tidak menggunakan prototipe (dengan asumsi itu adalah properti data, bukan penyetel).
Jika kita memanggil obj.method()
, dan method
diambil dari prototipe, this
masih merujuk pada obj
. Jadi metode selalu bekerja dengan objek saat ini meskipun metode tersebut diwariskan.
Perulangan for..in
mengulangi propertinya sendiri dan properti yang diwarisinya. Semua metode perolehan kunci/nilai lainnya hanya beroperasi pada objek itu sendiri.
pentingnya: 5
Berikut kode yang membuat sepasang objek, lalu memodifikasinya.
Nilai manakah yang ditampilkan dalam proses tersebut?
biarkan hewan = { melompat: nol }; biarkan kelinci = { __proto__: binatang, melompat: benar }; alert( kelinci.jumps ); // ? (1) hapus kelinci.jumps; alert( kelinci.jumps ); // ? (2) hapus animal.jumps; alert( kelinci.jumps ); // ? (3)
Seharusnya ada 3 jawaban.
true
, diambil dari rabbit
.
null
, diambil dari animal
.
undefined
, tidak ada properti seperti itu lagi.
pentingnya: 5
Tugas ini memiliki dua bagian.
Mengingat benda-benda berikut:
biarkan kepala = { kacamata: 1 }; misalkan tabel = { pena: 3 }; biarkan tidur = { lembar: 1, bantal: 2 }; biarkan kantong = { uang: 2000 };
Gunakan __proto__
untuk menetapkan prototipe sedemikian rupa sehingga setiap pencarian properti akan mengikuti jalurnya: pockets
→ bed
→ table
→ head
. Misalnya, pockets.pen
harus bernilai 3
(ditemukan di table
), dan bed.glasses
harus bernilai 1
(ditemukan di head
).
Jawab pertanyaannya: apakah lebih cepat mendapatkan glasses
sebagai pockets.glasses
atau head.glasses
? Patokan jika diperlukan.
Mari tambahkan __proto__
:
biarkan kepala = { kacamata: 1 }; misalkan tabel = { pena: 3, __proto__: kepala }; biarkan tidur = { lembar: 1, bantal: 2, __proto__: meja }; biarkan kantong = { uang: 2000, __proto__: tempat tidur }; waspada( kantong.pena ); // 3 alert( tempat tidur.kacamata ); // 1 waspada( meja.uang ); // belum diartikan
Dalam mesin modern, dari segi performa, tidak ada bedanya apakah kita mengambil properti dari suatu objek atau prototipenya. Mereka mengingat di mana properti itu ditemukan dan menggunakannya kembali pada permintaan berikutnya.
Misalnya, untuk pockets.glasses
mereka mengingat di mana mereka menemukan glasses
(di head
), dan lain kali akan mencari di sana. Mereka juga cukup pintar untuk memperbarui cache internal jika ada perubahan, sehingga optimasi aman.
pentingnya: 5
Kami memiliki rabbit
yang diwarisi dari animal
.
Jika kita memanggil rabbit.eat()
, objek manakah yang menerima properti full
: animal
atau rabbit
?
biarkan hewan = { makan() { ini.lengkap = benar; } }; biarkan kelinci = { __proto__: binatang }; kelinci.makan();
Jawabannya: rabbit
.
Itu karena this
adalah objek sebelum titik, jadi rabbit.eat()
memodifikasi rabbit
.
Pencarian dan eksekusi properti adalah dua hal yang berbeda.
Metode rabbit.eat
pertama kali ditemukan di prototipe, kemudian dieksekusi dengan this=rabbit
.
pentingnya: 5
Kami memiliki dua hamster: speedy
dan lazy
yang mewarisi objek hamster
pada umumnya.
Saat kita memberi makan salah satu dari mereka, yang lain juga kenyang. Mengapa? Bagaimana kita bisa memperbaikinya?
biarkan hamster = { perut: [], makan(makanan) { this.stomach.push(makanan); } }; biar cepat = { __proto__: hamster }; biar malas = { __proto__: hamster }; // Yang ini menemukan makanannya speedy.eat("apel"); waspada( cepat.perut ); // apel // Yang ini juga punya, kenapa? tolong perbaiki. waspada( malas.perut ); // apel
Mari kita perhatikan baik-baik apa yang terjadi dalam panggilan speedy.eat("apple")
.
Metode speedy.eat
terdapat pada prototype ( =hamster
), kemudian dieksekusi dengan this=speedy
(objek sebelum titik).
Kemudian this.stomach.push()
perlu menemukan properti stomach
dan memanggil push
di atasnya. Ia mencari stomach
di this
( =speedy
), tetapi tidak ditemukan.
Kemudian mengikuti rantai prototipe dan menemukan stomach
hamster
.
Kemudian ia memanggil push
, menambahkan makanan ke dalam perut prototipe .
Jadi semua hamster berbagi satu perut!
Baik untuk lazy.stomach.push(...)
dan speedy.stomach.push()
, properti stomach
ditemukan di prototipe (karena tidak ada di objek itu sendiri), lalu data baru dimasukkan ke dalamnya.
Harap dicatat bahwa hal seperti itu tidak terjadi dalam tugas sederhana this.stomach=
:
biarkan hamster = { perut: [], makan(makanan) { // tetapkan ke this.stomach, bukan this.stomach.push this.perut = [makanan]; } }; biar cepat = { __proto__: hamster }; biar malas = { __proto__: hamster }; // Yang cepat menemukan makanannya speedy.eat("apel"); waspada( cepat.perut ); // apel // Perut si pemalas kosong waspada( malas.perut ); // <tidak ada>
Sekarang semuanya berfungsi dengan baik, karena this.stomach=
tidak melakukan pencarian terhadap stomach
. Nilainya ditulis langsung ke objek this
.
Kita juga dapat menghindari masalah ini dengan memastikan setiap hamster mempunyai perutnya sendiri:
biarkan hamster = { perut: [], makan(makanan) { this.stomach.push(makanan); } }; biar cepat = { __proto__: hamster, perut: [] }; biar malas = { __proto__: hamster, perut: [] }; // Yang cepat menemukan makanannya speedy.eat("apel"); waspada( cepat.perut ); // apel // Perut si pemalas kosong waspada( malas.perut ); // <tidak ada>
Sebagai solusi umum, semua properti yang menggambarkan keadaan suatu benda tertentu, seperti stomach
di atas, harus dituliskan ke dalam benda tersebut. Hal ini mencegah masalah seperti itu.