Selamat sore teman! Kursus 
"Keamanan sistem informasi" telah diluncurkan, sehubungan dengan ini kami berbagi dengan Anda bagian akhir dari artikel "Dasar-dasar mesin JavaScript: optimalisasi prototipe", bagian pertama yang dapat dibaca di 
sini .
Kami juga mengingatkan Anda bahwa publikasi saat ini adalah kelanjutan dari dua artikel ini: 
βDasar-dasar mesin JavaScript: formulir umum dan caching Inline. Bagian 1 " , 
" Dasar-dasar mesin JavaScript: formulir umum dan caching Inline. Bagian 2 " .
 Kelas dan pemrograman prototipe
Kelas dan pemrograman prototipeSekarang setelah kita tahu cara mendapatkan akses cepat ke properti objek JavaScript, kita dapat melihat struktur kelas JavaScript yang lebih kompleks. Ini adalah apa yang tampak seperti sintaksis kelas dalam JavaScript:
class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } 
Meskipun ini tampaknya seperti konsep yang relatif baru untuk JavaScript, itu hanya "gula sintaksis" untuk pemrograman prototipe yang selalu digunakan dalam JavaScript:
 function Bar(x) { this.x = x; } Bar.prototype.getX = function getX() { return this.x; }; 
Di sini kita menetapkan properti 
getX ke objek 
getX . Ini akan berfungsi seperti halnya dengan objek lain, karena prototipe dalam JavaScript adalah objek yang sama. Dalam bahasa pemrograman prototipe seperti JavaScript, metode diakses melalui prototipe, sementara bidang disimpan dalam kasus tertentu.
Mari kita lihat lebih dekat apa yang terjadi ketika kita membuat instance baru dari 
Bar , yang akan kita sebut 
foo .
 const foo = new Bar(true); 
Sebuah instance yang dibuat menggunakan kode ini memiliki formulir dengan properti 
'x' tunggal. Prototipe 
foo adalah 
Bar.prototype , yang termasuk dalam kelas 
Bar .

Bar.prototype ini memiliki bentuk sendiri, berisi satu-satunya properti 
'getX' , yang nilainya ditentukan oleh fungsi 
'getX' , yang ketika dipanggil mengembalikan 
this.x Prototipe 
Bar.prototype adalah 
Object.prototype , yang merupakan bagian dari bahasa JavaScript. 
Object.prototype adalah akar dari pohon prototipe, sedangkan prototipenya adalah 
null .

Saat Anda membuat instance baru dari kelas yang sama, kedua instance memiliki bentuk yang sama, seperti yang sudah kita pahami. Kedua instance akan menunjuk ke objek 
Bar.prototype sama.
Akses properti prototipeNah, sekarang kita tahu apa yang terjadi ketika kita mendefinisikan kelas dan membuat instance baru. Tetapi apa yang terjadi jika kita memanggil metode pada contoh, seperti yang kita lakukan pada contoh berikut?
 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();  
Anda dapat mempertimbangkan pemanggilan metode apa pun sebagai dua langkah terpisah:
 const x = foo.getX();  
Langkah pertama adalah memuat metode, yang sebenarnya merupakan properti dari prototipe (yang nilainya adalah fungsi). Langkah kedua adalah memanggil fungsi dengan instance, misalnya, nilai 
this . Mari kita lihat lebih dekat pada langkah pertama di mana metode 
getX dari instance 
foo .

Mesin memulai turunan 
foo dan menyadari bahwa form 
foo tidak memiliki 
'getX' , sehingga harus melalui rantai prototipe untuk menemukannya. Kita sampai ke 
Bar.prototype , lihat pada bentuk prototipe, lihat bahwa ia memiliki properti 
'getX' dengan nol offset Kami mencari nilai pada offset ini di 
Bar.prototype dan menemukan 
JSFunction getX yang kami cari.
Fleksibilitas JavaScript memungkinkan tautan rantai prototipe berubah, misalnya:
 const foo = new Bar(true); foo.getX();  
Dalam contoh ini, kami menelepon
 foo.getX() 
dua kali, tetapi setiap kali memiliki arti dan hasil yang sama sekali berbeda. Itulah sebabnya, meskipun fakta bahwa prototipe hanyalah objek dalam JavaScript, mempercepat akses ke properti prototipe adalah tugas yang bahkan lebih penting untuk mesin JavaScript daripada mempercepat akses mereka sendiri ke properti pada objek biasa.
Dalam praktik sehari-hari, memuat properti prototipe adalah operasi yang cukup umum: ini terjadi setiap kali Anda memanggil metode!
 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX();  
Sebelumnya, kami berbicara tentang bagaimana mesin mengoptimalkan pemuatan properti reguler dengan menggunakan formulir dan cache inline. Bagaimana saya bisa mengoptimalkan pemuatan properti prototipe untuk objek dengan bentuk yang sama? Dari atas kita melihat bagaimana properti dimuat.

Untuk melakukan ini dengan cepat dengan unduhan berulang dalam kasus khusus ini, Anda perlu mengetahui tiga hal berikut:
- Bentuk footidak mengandung'getX'dan tidak berubah. Ini berarti bahwa tidak ada seorang pun yang mengubah objek foo dengan menambahkan atau menghapus properti atau mengubah salah satu atribut properti.
- Prototipe foo masih merupakan Bar.prototypeasli. Jadi tidak ada yang mengubah prototipefoomenggunakanObject.setPrototypeOf()atau menugaskannya ke properti_proto_khusus.
- Formulir Bar.prototypeberisi'getX'dan belum berubah. Ini berarti bahwa tidak ada yang mengubahBar.prototypedengan menambahkan atau menghapus properti atau mengubah salah satu atribut properti.
Dalam kasus umum, ini berarti bahwa Anda perlu membuat satu pemeriksaan instance itu sendiri dan dua pemeriksaan lagi untuk setiap prototipe hingga prototipe yang berisi properti yang diinginkan. 1 + 2N memeriksa, di mana N adalah jumlah prototipe yang digunakan, tidak terdengar begitu buruk dalam kasus ini, karena rantai prototipe relatif dangkal. Namun, mesin sering harus berurusan dengan rantai prototipe yang lebih lama, seperti halnya dengan kelas DOM reguler. Sebagai contoh:
 const anchor = document.createElement('a');  
Kami memiliki 
HTMLAnchorElement dan kami memanggil metode 
getAttribute() . Rantai untuk elemen sederhana ini sudah termasuk 6 prototipe! Sebagian besar metode DOM yang menarik minat kita bukan pada prototipe 
HTMLAnchorElement , tetapi di suatu tempat di rantai.

Metode 
getAttribute() ada di 
Element.prototype . Ini berarti bahwa setiap kali kita memanggil 
anchor.getAttribute() , mesin JavaScript perlu:
- Periksa bahwa 'getAttribute'bukan objekanchorper se;
- Verifikasi bahwa prototipe terakhir adalah HTMLAnchorElement.prototype;
- Konfirmasikan tidak adanya 'getAttribute'sana;
- Verifikasi bahwa prototipe berikutnya adalah HTMLElement.prototype;
- Konfirmasikan tidak adanya 'getAttribute';
- Verifikasi bahwa prototipe berikutnya adalah Element.prototype;
- Periksa apakah 'getAttribute'ada di dalamnya.
Sebanyak 7 cek. Karena jenis kode ini cukup umum di web, mesin menggunakan berbagai trik untuk mengurangi jumlah pemeriksaan yang diperlukan untuk memuat properti prototipe.
Kembali ke contoh sebelumnya di mana kami hanya melakukan tiga pemeriksaan ketika meminta 
'getX' untuk 
foo :
 class Bar { constructor(x) { this.x = x; } getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 
Untuk setiap objek yang terjadi sebelum prototipe yang berisi properti yang diinginkan, perlu untuk memeriksa formulir untuk tidak adanya properti ini. Alangkah baiknya jika kita bisa mengurangi jumlah cek dengan menghadirkan cek prototipe sebagai cek untuk tidak adanya properti. Intinya, inilah yang dilakukan mesin dengan trik sederhana: alih-alih menyimpan tautan prototipe ke instance itu sendiri, engine menyimpannya dalam bentuk.

Setiap bentuk menunjukkan prototipe. Ini berarti bahwa setiap kali prototipe 
foo berubah, mesin pindah ke bentuk baru. Sekarang kita perlu memeriksa hanya bentuk objek untuk mengkonfirmasi tidak adanya properti tertentu, serta melindungi tautan prototipe (menjaga tautan prototipe).
Dengan pendekatan ini, kami dapat mengurangi jumlah cek yang diperlukan dari 2N + 1 hingga 1 + N untuk mempercepat akses. Ini masih merupakan operasi yang cukup mahal, karena masih merupakan fungsi linier dari jumlah prototipe dalam rantai. Mesin menggunakan berbagai trik untuk lebih mengurangi jumlah cek ke nilai konstan tertentu, terutama dalam kasus pemuatan berurutan dari properti yang sama.
Sel validitasV8 memproses formulir prototipe khusus untuk tujuan ini. Setiap prototipe memiliki bentuk unik yang tidak dibagi dengan objek lain (khususnya, dengan prototipe lain), dan masing-masing bentuk prototipe ini memiliki 
ValidityCell khusus yang terkait dengannya.

ValidityCell dinonaktifkan setiap kali seseorang mengubah prototipe yang terkait dengannya atau prototipe lain di atasnya. Mari kita lihat cara kerjanya.
Untuk mempercepat unduhan prototipe berikutnya, V8 menempatkan cache Inline di lokasi empat bidang:

Ketika cache inline dipanaskan saat pertama kali kode dijalankan, V8 mengingat offset di mana properti ditemukan dalam prototipe, prototipe ini (misalnya, 
Bar.prototype ), bentuk instance (dalam kasus kami, form 
foo ), dan juga mengikat 
ValidityCell saat ini ke prototipe yang diterima dari contoh formulir (dalam kasus kami, 
Bar.prototype diambil).
Lain kali Anda menggunakan cache Inline, mesin perlu memeriksa formulir instance dan 
ValidityCell . Jika masih valid, mesin langsung menggunakan offset pada prototipe, melewatkan langkah-langkah pencarian tambahan.

Saat Anda mengubah prototipe, formulir baru disorot, dan sel 
ValidityCell sebelumnya dinonaktifkan. Karena itu, cache Inline dilewati saat berikutnya dimulai, yang berujung pada kinerja yang buruk.
Mari kita kembali ke contoh dengan elemen DOM. Setiap perubahan 
Object.prototype tidak hanya membatalkan cache Inline untuk 
Object.prototype , tetapi juga untuk setiap prototipe dalam rantai di bawahnya, termasuk 
EventTarget.prototype , 
Node.prototype , 
Element.prototype , dll., 
HTMLAnchorElement.prototype itu sendiri.

Bahkan, memodifikasi 
Object.prototype saat kode sedang dieksekusi adalah kehilangan kinerja yang mengerikan. Jangan lakukan ini!
Mari kita lihat contoh spesifik untuk lebih memahami bagaimana ini bekerja. Katakanlah kita memiliki kelas 
Bar dan fungsi 
loadX yang memanggil metode pada objek bertipe 
Bar . Kami memanggil fungsi 
loadX beberapa kali dengan instance dari kelas yang sama.
 class Bar {  } function loadX(bar) { return bar.getX();  
Cache sebaris di 
loadX sekarang menunjuk ke 
ValidityCell untuk 
Bar.prototype . Jika Anda kemudian memodifikasi (mutate) 
Object.prototype , yang merupakan akar dari semua prototipe dalam JavaScript, 
ValidityCell menjadi tidak valid dan cache Inline yang ada tidak akan digunakan lain kali, yang mengakibatkan kinerja buruk.
Mengubah 
Object.prototype selalu merupakan ide yang buruk, karena membatalkan semua cache Inline untuk prototipe yang dimuat pada saat perubahan. Ini adalah contoh bagaimana TIDAK harus dilakukan:
 Object.prototype.foo = function() {  };  
Kami sedang memperluas 
Object.prototype , yang membatalkan semua cache prototipe Inline yang dimuat oleh mesin pada saat ini. Kemudian kita akan menjalankan beberapa kode yang menggunakan metode yang dijelaskan oleh kita. Mesin harus mulai dari awal dan mengkonfigurasi cache Inline untuk setiap akses ke properti prototipe. Dan akhirnya, "bersihkan" dan hapus metode prototipe yang kami tambahkan sebelumnya.
Anda pikir membersihkan adalah ide yang bagus, bukan? Nah, dalam hal ini, ini akan semakin memperburuk situasi! Menghapus properti mengubah 
Object.prototype , sehingga semua cache Inline dinonaktifkan lagi, dan mesin harus mulai bekerja dari awal lagi.
Untuk meringkas . Terlepas dari kenyataan bahwa prototipe hanyalah objek, mereka secara khusus diproses oleh mesin JavaScript untuk mengoptimalkan kinerja pencarian metode oleh prototipe. 
Biarkan prototipe sendiri! Atau jika Anda benar-benar harus berurusan dengan mereka, lakukan sebelum menjalankan kode, sehingga Anda setidaknya tidak akan membatalkan semua upaya untuk mengoptimalkan kode Anda selama eksekusi!
Ringkaslah
Kami mempelajari bagaimana JavaScript menyimpan objek dan kelas, dan bagaimana formulir, cache inline, dan sel validitas membantu mengoptimalkan operasi prototipe. Berdasarkan pengetahuan ini, kami memahami cara meningkatkan kinerja dari sudut pandang praktis: jangan menyentuh prototipe! (atau jika Anda benar-benar membutuhkannya, lakukan sebelum menjalankan kode).
β 
Bagian pertamaApakah seri publikasi ini bermanfaat bagi Anda? Tulis di komentar.