Pewarisan kelas adalah cara satu kelas memperluas kelas lainnya.
Jadi kita dapat membuat fungsionalitas baru di atas fungsionalitas yang sudah ada.
Misalkan kita mempunyai kelas Animal
:
kelas Hewan { konstruktor(nama) { ini.kecepatan = 0; ini.nama = nama; } lari(kecepatan) { this.speed = kecepatan; alert(`${this.name} berjalan dengan kecepatan ${this.speed}.`); } berhenti() { ini.kecepatan = 0; alert(`${ini.nama} diam.`); } } biarkan hewan = hewan baru("Hewan saya");
Inilah cara kita dapat merepresentasikan objek animal
dan kelas Animal
secara grafis:
…Dan kami ingin membuat class Rabbit
yang lain.
Karena kelinci adalah hewan, kelas Rabbit
harus didasarkan pada Animal
, memiliki akses ke metode hewan, sehingga kelinci dapat melakukan apa yang dapat dilakukan oleh hewan “generik”.
Sintaks untuk memperluas kelas lain adalah: class Child extends Parent
.
Mari kita buat class Rabbit
yang mewarisi dari Animal
:
kelas Kelinci memperluas Hewan { bersembunyi() { alert(`${nama ini} disembunyikan!`); } } biarkan kelinci = kelinci baru("Kelinci Putih"); kelinci.run(5); // Kelinci Putih berlari dengan kecepatan 5. kelinci.sembunyikan(); // Kelinci Putih bersembunyi!
Objek kelas Rabbit
memiliki akses ke metode Rabbit
, seperti rabbit.hide()
, dan juga ke metode Animal
, seperti rabbit.run()
.
Secara internal, kata kunci extends
berfungsi menggunakan mekanisme prototipe lama yang bagus. Ini menetapkan Rabbit.prototype.[[Prototype]]
menjadi Animal.prototype
. Jadi, jika suatu metode tidak ditemukan di Rabbit.prototype
, JavaScript akan mengambilnya dari Animal.prototype
.
Misalnya, untuk menemukan metode rabbit.run
, mesin memeriksa (dari bawah ke atas pada gambar):
Objek rabbit
(tidak ada run
).
Prototipenya, yaitu Rabbit.prototype
(memiliki hide
, tetapi tidak run
).
Prototipenya, yaitu (karena extends
) Animal.prototype
, yang akhirnya memiliki metode run
.
Seperti yang dapat kita ingat dari bab Prototipe asli, JavaScript sendiri menggunakan pewarisan prototipe untuk objek bawaan. Misalnya Date.prototype.[[Prototype]]
adalah Object.prototype
. Itu sebabnya tanggal memiliki akses ke metode objek umum.
Ekspresi apa pun diperbolehkan setelah extends
Sintaks kelas memungkinkan untuk menentukan tidak hanya kelas, tetapi ekspresi apa pun setelahnya extends
.
Misalnya, pemanggilan fungsi yang menghasilkan kelas induk:
fungsi f(frasa) { kelas kembali { sayHi() { peringatan(frasa); } }; } kelas Pengguna memperluas f("Halo") {} Pengguna baru().sayHi(); // Halo
Di sini class User
mewarisi dari hasil f("Hello")
.
Itu mungkin berguna untuk pola pemrograman tingkat lanjut ketika kita menggunakan fungsi untuk menghasilkan kelas yang bergantung pada banyak kondisi dan dapat mewarisinya.
Sekarang mari kita maju dan mengganti suatu metode. Secara default, semua metode yang tidak ditentukan dalam class Rabbit
diambil langsung “apa adanya” dari class Animal
.
Namun jika kita menentukan metode kita sendiri di Rabbit
, seperti stop()
maka metode tersebut akan digunakan sebagai gantinya:
kelas Kelinci memperluas Hewan { berhenti() { // ...sekarang ini akan digunakan untuk kelinci.stop() // dari pada stop() dari kelas Animal } }
Biasanya, bagaimanapun, kita tidak ingin sepenuhnya mengganti metode induk, melainkan membangun di atasnya untuk mengubah atau memperluas fungsionalitasnya. Kami melakukan sesuatu dalam metode kami, tetapi memanggil metode induk sebelum/sesudahnya atau dalam proses.
Kelas menyediakan kata kunci "super"
untuk itu.
super.method(...)
untuk memanggil metode induk.
super(...)
untuk memanggil konstruktor induk (hanya di dalam konstruktor kita).
Misalnya, biarkan kelinci kita bersembunyi secara otomatis ketika dihentikan:
kelas Hewan { konstruktor(nama) { ini.kecepatan = 0; ini.nama = nama; } lari(kecepatan) { this.speed = kecepatan; alert(`${this.name} berjalan dengan kecepatan ${this.speed}.`); } berhenti() { ini.kecepatan = 0; alert(`${ini.nama} diam.`); } } kelas Kelinci memperluas Hewan { bersembunyi() { alert(`${nama ini} disembunyikan!`); } berhenti() { super.berhenti(); // panggil orang tua berhenti ini.sembunyikan(); // lalu sembunyikan } } biarkan kelinci = kelinci baru("Kelinci Putih"); kelinci.run(5); // Kelinci Putih berlari dengan kecepatan 5. kelinci.stop(); // Kelinci Putih diam. Kelinci Putih bersembunyi!
Sekarang Rabbit
memiliki metode stop
yang memanggil induk super.stop()
dalam prosesnya.
Fungsi panah tidak memiliki super
Seperti disebutkan dalam bab Fungsi panah yang ditinjau kembali, fungsi panah tidak memiliki super
.
Jika diakses, diambil dari fungsi luar. Misalnya:
kelas Kelinci memperluas Hewan { berhenti() { setTimeout(() => super.stop(), 1000); // panggil orang tua berhenti setelah 1 detik } }
Fungsi super
pada panah sama dengan stop()
, sehingga berfungsi sebagaimana mestinya. Jika kita menentukan fungsi “reguler” di sini, akan ada kesalahan:
// Super tak terduga setTimeout(fungsi() { super.stop() }, 1000);
Dengan konstruktor, ini menjadi sedikit rumit.
Hingga saat ini Rabbit
belum memiliki constructor
sendiri.
Menurut spesifikasinya, jika suatu kelas memperluas kelas lain dan tidak memiliki constructor
, maka constructor
“kosong” berikut akan dihasilkan:
kelas Kelinci memperluas Hewan { // dihasilkan untuk memperluas kelas tanpa konstruktor sendiri konstruktor(...args) { super(...args); } }
Seperti yang bisa kita lihat, pada dasarnya ia memanggil constructor
induk yang meneruskan semua argumennya. Itu terjadi jika kita tidak menulis konstruktor kita sendiri.
Sekarang mari tambahkan konstruktor khusus ke Rabbit
. Ini akan menentukan earLength
selain name
:
kelas Hewan { konstruktor(nama) { ini.kecepatan = 0; ini.nama = nama; } // ... } kelas Kelinci memperluas Hewan { konstruktor(nama, panjang telinga) { ini.kecepatan = 0; ini.nama = nama; this.earLength = panjang telinga; } // ... } // Tidak berhasil! biarkan kelinci = Kelinci baru("Kelinci Putih", 10); // Kesalahan: ini tidak ditentukan.
Ups! Kami mendapat kesalahan. Sekarang kita tidak bisa membuat kelinci. Apa yang salah?
Jawaban singkatnya adalah:
Konstruktor di kelas yang mewarisi harus memanggil super(...)
, dan (!) melakukannya sebelum menggunakan this
.
…Tapi kenapa? Apa yang terjadi di sini? Memang, persyaratan tersebut terkesan aneh.
Tentu saja ada penjelasannya. Mari kita bahas secara detail, sehingga Anda benar-benar memahami apa yang terjadi.
Dalam JavaScript, ada perbedaan antara fungsi konstruktor dari kelas yang mewarisi (disebut “konstruktor turunan”) dan fungsi lainnya. Konstruktor turunan memiliki properti internal khusus [[ConstructorKind]]:"derived"
. Itu label internal khusus.
Label itu memengaruhi perilakunya dengan new
.
Ketika fungsi reguler dijalankan dengan new
, ia membuat objek kosong dan menugaskannya ke this
.
Namun ketika konstruktor turunan berjalan, ia tidak melakukan hal ini. Ia mengharapkan konstruktor induk untuk melakukan pekerjaan ini.
Jadi konstruktor turunan harus memanggil super
untuk mengeksekusi konstruktor induknya (basis), jika tidak, objek untuk this
tidak akan dibuat. Dan kita akan mendapatkan kesalahan.
Agar konstruktor Rabbit
dapat berfungsi, ia perlu memanggil super()
sebelum menggunakan this
, seperti di sini:
kelas Hewan { konstruktor(nama) { ini.kecepatan = 0; ini.nama = nama; } // ... } kelas Kelinci memperluas Hewan { konstruktor(nama, panjang telinga) { super(nama); this.earLength = panjang telinga; } // ... } // sekarang baik-baik saja biarkan kelinci = kelinci baru("Kelinci Putih", 10); alert(kelinci.nama); // Kelinci Putih alert(kelinci.earLength); // 10
Catatan lanjutan
Catatan ini mengasumsikan Anda memiliki pengalaman tertentu dengan kelas, mungkin dalam bahasa pemrograman lain.
Ini memberikan wawasan yang lebih baik tentang bahasa tersebut dan juga menjelaskan perilaku yang mungkin menjadi sumber bug (tetapi tidak terlalu sering).
Jika Anda merasa sulit untuk memahaminya, lanjutkan saja, lanjutkan membaca, lalu kembali lagi beberapa waktu kemudian.
Kita tidak hanya dapat mengganti metode, tetapi juga bidang kelas.
Meskipun demikian, ada perilaku rumit ketika kita mengakses bidang yang diganti di konstruktor induk, sangat berbeda dari kebanyakan bahasa pemrograman lainnya.
Perhatikan contoh ini:
kelas Hewan { nama = 'hewan'; konstruktor() { alert(ini.nama); // (*) } } kelas Kelinci memperluas Hewan { nama = 'kelinci'; } Hewan baru(); // binatang Kelinci baru(); // binatang
Di sini, kelas Rabbit
memperluas Animal
dan menimpa bidang name
dengan nilainya sendiri.
Tidak ada konstruktor sendiri di Rabbit
, jadi konstruktor Animal
dipanggil.
Yang menarik adalah dalam kedua kasus: new Animal()
dan new Rabbit()
, alert
di baris (*)
menunjukkan animal
.
Dengan kata lain, konstruktor induk selalu menggunakan nilai bidangnya sendiri, bukan nilai yang diganti.
Apa yang aneh tentang itu?
Jika masih belum jelas, silakan bandingkan dengan metode.
Ini kode yang sama, namun alih-alih menggunakan bidang this.name
kita memanggil metode this.showName()
:
kelas Hewan { showName() { // dari pada ini.nama = 'hewan' waspada('hewan'); } konstruktor() { ini.showName(); // dari pada alert(ini.nama); } } kelas Kelinci memperluas Hewan { tampilkanNama() { waspada('kelinci'); } } Hewan baru(); // binatang Kelinci baru(); // kelinci
Harap dicatat: sekarang hasilnya berbeda.
Dan itulah yang secara alami kita harapkan. Ketika konstruktor induk dipanggil di kelas turunan, ia menggunakan metode yang diganti.
…Tetapi untuk bidang kelas tidak demikian. Seperti disebutkan, konstruktor induk selalu menggunakan bidang induk.
Mengapa ada perbedaan?
Alasannya adalah urutan inisialisasi lapangan. Bidang kelas diinisialisasi:
Sebelum konstruktor untuk kelas dasar (yang tidak memperluas apa pun),
Segera setelah super()
untuk kelas turunan.
Dalam kasus kami, Rabbit
adalah kelas turunannya. Tidak ada constructor()
di dalamnya. Seperti yang dikatakan sebelumnya, itu sama seperti jika ada konstruktor kosong yang hanya berisi super(...args)
.
Jadi, new Rabbit()
memanggil super()
, sehingga mengeksekusi konstruktor induk, dan (sesuai aturan untuk kelas turunan) hanya setelah itu bidang kelasnya diinisialisasi. Pada saat konstruktor induk dieksekusi, belum ada field kelas Rabbit
, oleh karena itu digunakan field Animal
.
Perbedaan halus antara bidang dan metode ini khusus untuk JavaScript.
Untungnya, perilaku ini hanya muncul jika bidang yang diganti digunakan di konstruktor induk. Maka mungkin sulit untuk memahami apa yang terjadi, jadi kami menjelaskannya di sini.
Jika ini menjadi masalah, seseorang dapat memperbaikinya dengan menggunakan metode atau pengambil/penyetel alih-alih bidang.
Informasi lanjutan
Jika Anda membaca tutorial untuk pertama kalinya – bagian ini mungkin dilewati.
Ini tentang mekanisme internal di balik warisan dan super
.
Mari kita bahas lebih dalam tentang super
. Kita akan melihat beberapa hal menarik sepanjang perjalanan.
Pertama-tama, dari semua yang telah kita pelajari hingga saat ini, mustahil super
bisa berhasil sama sekali!
Ya, tentu saja, mari kita bertanya pada diri sendiri, bagaimana cara kerjanya secara teknis? Saat metode objek dijalankan, ia mendapatkan objek saat ini sebagai this
. Jika kita memanggil super.method()
, mesin perlu mendapatkan method
dari prototipe objek saat ini. Tapi bagaimana caranya?
Tugas ini mungkin tampak sederhana, namun sebenarnya tidak. Mesin mengetahui objek saat ini this
, sehingga bisa mendapatkan method
induk sebagai this.__proto__.method
. Sayangnya, solusi “naif” seperti itu tidak akan berhasil.
Mari kita tunjukkan masalahnya. Tanpa kelas, menggunakan objek biasa demi kesederhanaan.
Anda dapat melewati bagian ini dan melanjutkan ke subbagian [[HomeObject]]
jika Anda tidak ingin mengetahui detailnya. Itu tidak akan merugikan. Atau baca terus jika Anda tertarik untuk memahami berbagai hal secara mendalam.
Pada contoh di bawah, rabbit.__proto__ = animal
. Sekarang mari kita coba: di rabbit.eat()
kita akan memanggil animal.eat()
, menggunakan this.__proto__
:
biarkan hewan = { nama: "Hewan", makan() { alert(`${nama ini} makan.`); } }; biarkan kelinci = { __proto__: binatang, nama: "Kelinci", makan() { // begitulah cara kerja super.eat() this.__proto__.eat.call(ini); // (*) } }; kelinci.makan(); // Kelinci makan.
Pada baris (*)
kita mengambil eat
dari prototipe ( animal
) dan menyebutnya dalam konteks objek saat ini. Harap perhatikan bahwa .call(this)
penting di sini, karena this.__proto__.eat()
yang sederhana akan mengeksekusi parent eat
dalam konteks prototipe, bukan objek saat ini.
Dan pada kode di atas, ini benar-benar berfungsi sebagaimana mestinya: kita memiliki alert
yang benar.
Sekarang mari tambahkan satu objek lagi ke rantai. Kita akan melihat bagaimana keadaannya:
biarkan hewan = { nama: "Hewan", makan() { alert(`${nama.ini} makan.`); } }; biarkan kelinci = { __proto__: binatang, makan() { // ...memantul dengan gaya kelinci dan memanggil metode induk (hewan). this.__proto__.eat.call(ini); // (*) } }; misalkan telinga panjang = { __proto__: kelinci, makan() { // ...lakukan sesuatu dengan telinga panjang dan panggil metode induk (kelinci). this.__proto__.eat.call(ini); // (**) } }; telinga panjang.makan(); // Kesalahan: Ukuran tumpukan panggilan maksimum terlampaui
Kodenya tidak berfungsi lagi! Kita dapat melihat kesalahan saat mencoba memanggil longEar.eat()
.
Ini mungkin tidak terlalu jelas, tetapi jika kita melacak panggilan longEar.eat()
, kita dapat mengetahui alasannya. Di kedua baris (*)
dan (**)
nilai this
adalah objek saat ini ( longEar
). Itu penting: semua metode objek mendapatkan objek saat ini sebagai this
, bukan prototipe atau semacamnya.
Jadi, di kedua baris (*)
dan (**)
nilai this.__proto__
sama persis: rabbit
. Mereka berdua memanggil rabbit.eat
tanpa menaikkan rantai dalam putaran tanpa akhir.
Berikut gambaran apa yang terjadi:
Di dalam longEar.eat()
, baris (**)
memanggil rabbit.eat
dengan menyediakan this=longEar
.
// di dalam longEar.eat() kita memiliki ini = longEar ini.__proto__.makan.panggilan(ini) // (**) // menjadi telinga panjang.__proto__.makan.panggilan(ini) // itu kelinci.makan.panggilan(ini);
Kemudian pada baris (*)
dari rabbit.eat
, kita ingin meneruskan panggilan tersebut lebih tinggi lagi dalam rantai tersebut, namun this=longEar
, jadi this.__proto__.eat
kembali menjadi rabbit.eat
!
// di dalam kelinci.eat() kita juga punya ini = longEar ini.__proto__.makan.panggilan(ini) // (*) // menjadi telinga panjang.__proto__.makan.panggilan(ini) // atau (lagi) kelinci.makan.panggilan(ini);
…Jadi rabbit.eat
menyebut dirinya dalam lingkaran tanpa akhir, karena ia tidak bisa naik lebih jauh.
Masalahnya tidak bisa diselesaikan hanya dengan menggunakan this
saja.
[[HomeObject]]
Untuk memberikan solusinya, JavaScript menambahkan satu lagi properti internal khusus untuk fungsi: [[HomeObject]]
.
Ketika suatu fungsi ditentukan sebagai kelas atau metode objek, properti [[HomeObject]]
-nya menjadi objek tersebut.
Kemudian super
menggunakannya untuk menyelesaikan prototipe induk dan metodenya.
Mari kita lihat cara kerjanya, pertama dengan objek biasa:
biarkan hewan = { nama: "Hewan", makan() { // hewan.makan.[[Objek Rumah]] == hewan alert(`${nama.ini} makan.`); } }; biarkan kelinci = { __proto__: binatang, nama: "Kelinci", makan() { // kelinci.makan.[[Objek Rumah]] == kelinci super.makan(); } }; misalkan telinga panjang = { __proto__: kelinci, nama: "Telinga Panjang", makan() { // longEar.eat.[[HomeObject]] == longEar super.makan(); } }; // bekerja dengan benar telinga panjang.makan(); // Telinga Panjang makan.
Ini berfungsi sebagaimana mestinya, karena mekanisme [[HomeObject]]
. Suatu metode, seperti longEar.eat
, mengetahui [[HomeObject]]
miliknya dan mengambil metode induk dari prototipenya. Tanpa menggunakan this
.
Seperti yang telah kita ketahui sebelumnya, umumnya fungsi bersifat “bebas”, tidak terikat pada objek di JavaScript. Jadi mereka dapat disalin antar objek dan dipanggil dengan this
lainnya.
Keberadaan [[HomeObject]]
melanggar prinsip tersebut, karena metode mengingat objeknya. [[HomeObject]]
tidak dapat diubah, jadi ikatan ini selamanya.
Satu-satunya tempat dalam bahasa di mana [[HomeObject]]
digunakan – adalah super
. Jadi, jika suatu metode tidak menggunakan super
, maka kita masih bisa menganggapnya gratis dan menyalin antar objek. Namun dengan hal-hal super
, hal-hal mungkin salah.
Berikut demo hasil super
yang salah setelah disalin:
biarkan hewan = { ucapkan Hai() { alert(`Saya adalah binatang`); } }; // kelinci mewarisi hewan biarkan kelinci = { __proto__: binatang, ucapkan Hai() { super.sayHi(); } }; biarkan menanam = { ucapkan Hai() { alert("Saya adalah tanaman"); } }; // pohon mewarisi tanaman biarkan pohon = { __proto__: tanaman, sayHi: kelinci.sayHi // (*) }; pohon.sayHi(); // Aku seekor binatang (?!?)
Panggilan ke tree.sayHi()
menunjukkan “Saya adalah binatang”. Pasti salah.
Alasannya sederhana:
Pada baris (*)
, metode tree.sayHi
disalin dari rabbit
. Mungkin kami hanya ingin menghindari duplikasi kode?
[[HomeObject]]
miliknya adalah rabbit
, seperti yang dibuat pada rabbit
. Tidak ada cara untuk mengubah [[HomeObject]]
.
Kode tree.sayHi()
memiliki super.sayHi()
di dalamnya. Itu muncul dari rabbit
dan mengambil metode dari animal
.
Berikut diagram yang terjadi:
[[HomeObject]]
didefinisikan untuk metode baik di kelas maupun di objek biasa. Namun untuk objek, metode harus ditentukan persis sebagai method()
, bukan sebagai "method: function()"
.
Perbedaannya mungkin tidak penting bagi kami, namun penting untuk JavaScript.
Pada contoh di bawah, sintaks non-metode digunakan untuk perbandingan. Properti [[HomeObject]]
tidak disetel dan warisan tidak berfungsi:
biarkan hewan = { makan: function() { // sengaja menulis seperti ini, bukannya makan() {... // ... } }; biarkan kelinci = { __proto__: binatang, makan: fungsi() { super.makan(); } }; kelinci.makan(); // Kesalahan memanggil super (karena tidak ada [[HomeObject]])
Untuk memperluas kelas: class Child extends Parent
:
Itu berarti Child.prototype.__proto__
akan menjadi Parent.prototype
, sehingga metode diwariskan.
Saat mengganti konstruktor:
Kita harus memanggil konstruktor induk sebagai super()
di konstruktor Child
sebelum menggunakan this
.
Saat mengganti metode lain:
Kita dapat menggunakan super.method()
dalam metode Child
untuk memanggil metode Parent
.
Internal:
Metode mengingat kelas/objeknya di properti internal [[HomeObject]]
. Begitulah cara super
menyelesaikan metode induk.
Jadi tidak aman untuk menyalin metode dengan super
dari satu objek ke objek lainnya.
Juga:
Fungsi panah tidak memiliki this
atau super
sendiri, sehingga secara transparan sesuai dengan konteks sekitarnya.
pentingnya: 5
Berikut kode dengan Rabbit
yang memperluas Animal
.
Sayangnya, objek Rabbit
tidak dapat dibuat. Ada apa? Perbaiki.
kelas Hewan { konstruktor(nama) { ini.nama = nama; } } kelas Kelinci memperluas Hewan { konstruktor(nama) { ini.nama = nama; this.created = Tanggal.sekarang(); } } biarkan kelinci = kelinci baru("Kelinci Putih"); // Kesalahan: ini tidak ditentukan alert(kelinci.nama);
Itu karena konstruktor anak harus memanggil super()
.
Berikut kode yang diperbaiki:
kelas Hewan { konstruktor(nama) { ini.nama = nama; } } kelas Kelinci memperluas Hewan { konstruktor(nama) { super(nama); this.created = Tanggal.sekarang(); } } biarkan kelinci = kelinci baru("Kelinci Putih"); // oke sekarang alert(kelinci.nama); // Kelinci Putih
pentingnya: 5
Kami punya kelas Clock
. Saat ini, ia mencetak waktu setiap detik.
kelas Jam { konstruktor({ templat }) { this.template = templat; } memberikan() { biarkan tanggal = Tanggal baru(); biarkan jam = tanggal.getHours(); if (jam < 10) jam = '0' + jam; biarkan menit = tanggal.getMinutes(); if (menit < 10) menit = '0' + menit; biarkan detik = tanggal.getSeconds(); if (detik < 10) detik = '0' + detik; biarkan keluaran = ini.template .replace('h', jam) .replace('m', menit) .replace('s', detik); konsol.log(keluaran); } berhenti() { clearInterval(ini.timer); } awal() { ini.render(); this.timer = setInterval(() => this.render(), 1000); } }
Buat kelas baru ExtendedClock
yang mewarisi dari Clock
dan menambahkan parameter precision
– jumlah ms
di antara “centang”. Seharusnya 1000
(1 detik) secara default.
Kode Anda harus ada di file extended-clock.js
Jangan ubah clock.js
yang asli. Perluas itu.
Buka kotak pasir untuk tugas tersebut.
kelas ExtendedClock memperluas Jam { konstruktor(pilihan) { super(pilihan); misalkan { presisi = 1000 } = pilihan; this.presisi = presisi; } awal() { ini.render(); this.timer = setInterval(() => this.render(), this.precision); } };
Buka solusi di kotak pasir.