Optimalisasi kerja dengan prototipe di mesin JavaScript

Bahannya, terjemahan yang kami terbitkan hari ini, disiapkan oleh Matthias Binens dan Benedict Meirer. Mereka sedang mengerjakan mesin V8 JS di Google. Artikel ini dikhususkan untuk beberapa mekanisme dasar yang menjadi karakteristik tidak hanya untuk V8, tetapi juga untuk mesin lainnya. Keakraban dengan struktur internal mekanisme tersebut memungkinkan mereka yang terlibat dalam pengembangan JavaScript untuk lebih menavigasi masalah kinerja kode. Secara khusus, di sini kita akan berbicara tentang fitur-fitur dari pipa optimasi mesin, dan bagaimana mempercepat akses ke properti prototipe objek.



Level dan Trade-off Optimasi Kode


Proses konversi teks program yang ditulis dalam JavaScript ke dalam kode yang sesuai untuk eksekusi terlihat kurang lebih sama di mesin yang berbeda.
Proses konversi kode sumber JS ke kode yang dapat dieksekusi

Detail dapat ditemukan di sini . Selain itu, perlu dicatat bahwa meskipun, pada tingkat tinggi, pipa untuk mengubah kode sumber menjadi executable sangat mirip untuk mesin yang berbeda, sistem optimisasi kode mereka sering berbeda. Kenapa begitu? Mengapa beberapa mesin memiliki tingkat optimisasi yang lebih tinggi daripada yang lain? Ternyata mesin harus berkompromi dengan satu atau lain cara, yang terdiri dari kenyataan bahwa mereka dapat dengan cepat menghasilkan kode yang bukan yang paling efisien tetapi cocok untuk dieksekusi, atau menghabiskan lebih banyak waktu membuat kode seperti itu, tetapi karena ini, mencapai kinerja optimal.
Persiapan kode yang cepat untuk eksekusi dan kode yang dioptimalkan yang membutuhkan waktu lebih lama tetapi berjalan lebih cepat

Interpreter dapat dengan cepat menghasilkan bytecode, tetapi kode seperti itu biasanya tidak terlalu efisien. Sebaliknya, kompiler pengoptimalisasi membutuhkan lebih banyak waktu untuk menghasilkan kode, tetapi pada akhirnya akan dioptimalkan, kode mesin lebih cepat.

Model penyiapan kode untuk eksekusi inilah yang digunakan dalam V8. Interpreter V8 disebut Ignition, itu adalah yang tercepat dari interpreter yang ada (dalam hal mengeksekusi kode sumber sumber). Kompiler V8 yang optimal disebut TurboFan, yang bertanggung jawab untuk membuat kode mesin yang sangat optimal.
Interpreter pengapian dan kompiler mengoptimalkan TurboFan

Pertukaran antara penundaan dalam memulai program dan kecepatan eksekusi adalah alasan bahwa beberapa mesin JS memiliki tingkat optimasi tambahan. Sebagai contoh, di SpiderMonkey, antara penerjemah dan pengoptimal pengoptimal IonMonkey, ada tingkat menengah yang diwakili oleh kompiler dasar (ini disebut "The Baseline Compiler" dalam dokumentasi Mozilla, tetapi "baseline" bukan nama yang tepat).
Level optimisasi kode SpiderMonkey

Interpreter dengan cepat menghasilkan bytecode, tetapi kode tersebut dijalankan relatif lambat. Kompilator dasar membutuhkan waktu lebih lama untuk menghasilkan kode, tetapi kode ini sudah lebih cepat. Akhirnya, kompiler IonMonkey yang mengoptimalkan membutuhkan waktu paling lama untuk menghasilkan kode mesin, tetapi kode ini dapat dijalankan dengan sangat efisien.

Mari kita lihat contoh spesifik dan lihat bagaimana pipa berbagai mesin menangani kode. Dalam contoh yang disajikan di sini, ada loop "panas" yang berisi kode yang berulang berkali-kali.

let result = 0; for (let i = 0; i < 4242424242; ++i) {    result += i; } console.log(result); 

V8 mulai mengeksekusi bytecode dalam interpreter Ignition. Di beberapa titik waktu, mesin menemukan bahwa kode "panas" dan meluncurkan frontend TurboFan, yang merupakan bagian dari TurboFan yang bekerja dengan profil data dan menciptakan representasi dasar mesin dari kode. Data kemudian diteruskan ke pengoptimal TurboFan, yang beroperasi dalam aliran terpisah, untuk peningkatan lebih lanjut.
Optimasi Hot Code di V8

Selama optimasi, V8 terus mengeksekusi bytecode di Ignition. Ketika optimizer selesai, kami memiliki kode mesin yang dapat dieksekusi yang dapat digunakan di masa mendatang.

Mesin SpiderMonkey juga mulai mengeksekusi bytecode pada interpreter. Tetapi memiliki level tambahan yang diwakili oleh kompiler dasar, yang mengarah pada fakta bahwa kode "panas" pertama kali sampai ke kompiler ini. Ini menghasilkan kode dasar di utas utama, transisi ke pelaksanaan kode ini dibuat ketika sudah siap.
Optimasi Kode Populer di SpiderMonkey

Jika kode dasar berjalan cukup lama, SpiderMonkey akhirnya meluncurkan antarmuka dan pengoptimal IonMonkey, yang sangat mirip dengan apa yang terjadi di V8. Kode dasar terus berjalan sebagai bagian dari proses optimasi kode yang dilakukan oleh IonMonkey. Akibatnya, ketika optimisasi selesai, kode yang dioptimalkan dieksekusi alih-alih kode dasar.

Arsitektur mesin Chakra sangat mirip dengan arsitektur SpiderMonkey, tetapi Chakra berusaha untuk tingkat konkurensi yang lebih tinggi untuk menghindari pemblokiran utas utama. Alih-alih menyelesaikan tugas kompilasi di utas utama, Chakra menyalin dan mengirim bytecode dan profil data yang mungkin dibutuhkan oleh kompiler ke dalam proses kompilasi yang terpisah.
Optimasi kode panas di Chakra

Ketika kode yang dihasilkan disiapkan oleh SimpleJIT siap, mesin akan menjalankannya alih-alih bytecode. Proses ini diulangi untuk melanjutkan dengan eksekusi kode yang disiapkan oleh FullJIT. Keuntungan dari pendekatan ini adalah bahwa jeda yang terkait dengan menyalin data biasanya jauh lebih pendek daripada yang disebabkan oleh pengoperasian kompiler penuh (front-end). Namun, minus dari pendekatan ini adalah kenyataan bahwa algoritma penyalinan heuristik mungkin kehilangan beberapa informasi yang mungkin berguna untuk beberapa jenis optimasi. Di sini kita melihat contoh kompromi antara kualitas kode yang diterima dan penundaan.

Di JavaScriptCore, semua tugas kompilasi yang mengoptimalkan dilakukan secara paralel dengan utas utama yang bertanggung jawab untuk mengeksekusi kode JavaScript. Namun, tidak ada tahap penyalinan. Sebagai gantinya, utas utama hanya memanggil tugas kompilasi di utas lain. Kompiler kemudian menggunakan skema penguncian yang kompleks untuk mengakses profil data dari utas utama.
Optimalisasi kode "panas" di JavaScriptCore

Keuntungan dari pendekatan ini adalah ia mengurangi pemblokiran paksa dari utas utama yang disebabkan oleh fakta bahwa ia melakukan tugas-tugas optimasi kode. Kerugian dari arsitektur ini adalah bahwa implementasinya membutuhkan solusi dari tugas-tugas kompleks dari pemrosesan data multi-threaded, dan bahwa dalam proses pekerjaan, untuk melakukan berbagai operasi, kita harus menggunakan kunci.

Kami baru saja mendiskusikan trade-off yang terpaksa dilakukan oleh mesin, memilih antara pembuatan kode cepat menggunakan interpreter dan membuat kode cepat menggunakan kompilator pengoptimal. Namun, ini jauh dari semua masalah yang dihadapi mesin. Memori adalah sumber daya sistem lain saat Anda harus menggunakan solusi kompromi. Untuk menunjukkan ini, pertimbangkan program JS sederhana yang menambahkan angka.

 function add(x, y) {   return x + y; } add(1, 2); 

Berikut ini bytecode dari fungsi add dihasilkan oleh interpreter Ignition di V8:

 StackCheck Ldar a1 Add a0, [0] Return 

Anda tidak bisa masuk ke makna bytecode ini, pada kenyataannya, isinya tidak menarik bagi kami. Hal utama di sini adalah hanya memiliki empat instruksi.

Ketika sepotong kode seperti itu "panas", TurboFan diambil, yang menghasilkan kode mesin yang sangat dioptimalkan berikut:

 leaq rcx,[rip+0x0] movq rcx,[rcx-0x37] testb [rcx+0xf],0x1 jnz CompileLazyDeoptimizedCode push rbp movq rbp,rsp push rsi push rdi cmpq rsp,[r13+0xe88] jna StackOverflow movq rax,[rbp+0x18] test al,0x1 jnz Deoptimize movq rbx,[rbp+0x10] testb rbx,0x1 jnz Deoptimize movq rdx,rbx shrq rdx, 32 movq rcx,rax shrq rcx, 32 addl rdx,rcx jo Deoptimize shlq rdx, 32 movq rax,rdx movq rsp,rbp pop rbp ret 0x18 

Seperti yang Anda lihat, volume kode, dibandingkan dengan contoh empat instruksi di atas, sangat besar. Biasanya, bytecode jauh lebih kompak daripada kode mesin, dan khususnya kode mesin yang dioptimalkan. Di sisi lain, seorang juru bahasa diperlukan untuk menjalankan bytecode, dan kode yang dioptimalkan dapat dieksekusi langsung pada prosesor.
Ini adalah salah satu alasan utama mengapa mesin JavaScript tidak sepenuhnya mengoptimalkan semua kode. Seperti yang kita lihat sebelumnya, membuat kode mesin yang dioptimalkan membutuhkan banyak waktu, dan terlebih lagi, seperti yang baru saja kita ketahui, dibutuhkan lebih banyak memori untuk menyimpan kode mesin yang dioptimalkan.
Penggunaan memori dan tingkat optimisasi

Sebagai hasilnya, kita dapat mengatakan bahwa alasan mesin JS memiliki tingkat optimasi yang berbeda adalah masalah mendasar dalam memilih antara pembuatan kode cepat, misalnya, menggunakan juru bahasa, dan pembuatan kode cepat, yang dijalankan dengan menggunakan kompilator pengoptimal. Jika kita berbicara tentang tingkat optimasi kode yang digunakan dalam mesin, maka semakin banyak dari mereka, optimasi kode lebih halus dapat dikenakan, tetapi ini dicapai karena kompleksitas mesin dan karena beban tambahan pada sistem. Selain itu, di sini kita tidak boleh lupa bahwa tingkat optimisasi kode memengaruhi jumlah memori yang ditempati kode ini. Itulah sebabnya mesin JS hanya mencoba mengoptimalkan fungsi "panas".

Optimalisasi akses ke properti prototipe objek


Mesin JavaScript mengoptimalkan akses ke properti objek melalui penggunaan yang disebut objek bentuk (Bentuk) dan cache inline (Inline Cache, IC). Detail tentang ini dapat dibaca dalam materi ini , tetapi untuk membuatnya singkat, kita dapat mengatakan bahwa mesin menyimpan bentuk objek secara terpisah dari nilai-nilai objek.
Objek yang memiliki bentuk yang sama

Menggunakan bentuk-bentuk objek memungkinkan untuk melakukan optimasi yang disebut inline caching. Penggunaan bersama bentuk objek dan cache inline memungkinkan Anda untuk mempercepat operasi berulang untuk mengakses properti objek, dilakukan dari tempat yang sama dalam kode.
Mempercepat akses ke properti objek

Kelas dan Prototipe


Sekarang kita tahu bagaimana mempercepat akses ke properti objek dalam JavaScript, lihatlah salah satu inovasi JavaScript terbaru - kelas. Beginilah bentuk deklarasi kelas:

 class Bar {   constructor(x) {       this.x = x;   }   getX() {       return this.x;   } } 

Meskipun mungkin terlihat seperti penampilan dalam JS dari konsep yang sama sekali baru, kelas sebenarnya hanya gula sintaksis untuk sistem prototipe untuk membangun objek, yang selalu ada dalam JavaScript:

 function Bar(x) {   this.x = x; } Bar.prototype.getX = function getX() {   return this.x; }; 

Di sini kita menulis fungsi ke properti getX dari objek getX . Operasi ini bekerja dengan cara yang persis sama seperti ketika membuat properti dari objek lain, karena prototipe dalam JavaScript adalah objek. Dalam bahasa yang didasarkan pada penggunaan prototipe, seperti JavaScript, metode yang dapat dibagikan oleh semua objek dari jenis tertentu disimpan dalam prototipe, dan bidang objek individu disimpan dalam instance mereka.

Mari kita lihat apa yang terjadi, sehingga, di belakang layar ketika kita membuat instance baru dari objek Bar , menugaskannya ke foo konstan.

 const foo = new Bar(true); 

Setelah mengeksekusi kode tersebut, turunan dari objek yang dibuat di sini akan memiliki bentuk yang berisi satu properti x . Prototipe objek foo adalah Bar.prototype , yang termasuk dalam kelas Bar .
Obyek dan prototipe-nya

Bar.prototype memiliki bentuknya sendiri yang berisi properti getX tunggal yang nilainya adalah fungsi yang, ketika dipanggil, mengembalikan nilai this.x Prototipe prototipe Bar.prototype adalah Object.prototype , yang merupakan bagian dari bahasa. Object.prototype adalah elemen root dari pohon prototipe, jadi prototipenya adalah null .

Sekarang mari kita lihat apa yang terjadi jika Anda membuat objek tipe Bar .
Beberapa objek dengan tipe yang sama

Seperti yang Anda lihat, baik objek foo dan objek qux , yang merupakan instance dari kelas Bar , seperti yang telah kita katakan, menggunakan bentuk objek yang sama. Keduanya menggunakan prototipe yang sama - objek Bar.prototype .

Akses properti prototipe


Jadi sekarang kita tahu apa yang terjadi ketika kita mendeklarasikan kelas baru dan membuat instance. Dan bagaimana dengan panggilan ke metode objek? Pertimbangkan potongan kode berikut:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const x = foo.getX(); //        ^^^^^^^^^^ 

Panggilan metode dapat dipahami sebagai operasi yang terdiri dari dua langkah:

 const x = foo.getX(); //         : const $getX = foo.getX; const x = $getX.call(foo); 

Pada langkah pertama, metode ini dimuat, yang hanya merupakan properti dari prototipe (yang nilainya adalah fungsi). Pada langkah kedua, fungsi dipanggil dengan set this . Pertimbangkan langkah pertama dalam memuat metode getX dari objek foo :
Memuat metode getX dari objek foo

Mesin menganalisis objek foo dan menemukan bahwa tidak ada properti getX dalam bentuk objek foo . Ini berarti bahwa mesin perlu melihat rantai prototipe objek untuk menemukan metode ini. Mesin mengakses prototipe Bar.prototype dan melihat bentuk objek dari prototipe ini. Di sana, ia menemukan properti yang diinginkan pada offset 0. Selanjutnya, nilai yang disimpan pada offset ini di Bar.prototype , JSFunction getX terdeteksi di sana - dan inilah yang sebenarnya kami cari. Ini melengkapi pencarian untuk metode ini.

Fleksibilitas JavaScript memungkinkan untuk mengubah rantai prototipe. Misalnya, seperti ini:

 const foo = new Bar(true); foo.getX(); // true Object.setPrototypeOf(foo, null); foo.getX(); // Uncaught TypeError: foo.getX is not a function 

Dalam contoh ini, kami memanggil metode foo.getX() dua kali, tetapi masing-masing panggilan ini memiliki arti dan hasil yang sama sekali berbeda. Itulah sebabnya, meskipun prototipe JavaScript hanyalah objek, mempercepat akses ke properti prototipe bahkan lebih sulit untuk mesin JS daripada mempercepat akses ke properti mereka sendiri dari objek biasa.

Jika kita melihat program kehidupan nyata, ternyata memuat properti prototipe adalah operasi yang sangat umum. Ini dieksekusi setiap kali sebuah metode dipanggil.

 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 objek kustom biasa, properti melalui penggunaan bentuk objek dan cache inline. Bagaimana cara mengoptimalkan properti prototipe berulang memuat untuk objek dengan bentuk yang sama? Di atas, kami melihat cara properti dimuat.
Memuat metode getX dari objek foo

Untuk mempercepat akses ke metode dengan panggilan berulang ke dalamnya, dalam kasus kami, Anda perlu mengetahui yang berikut:

  1. Bentuk objek foo tidak mengandung metode getX dan tidak berubah. Ini berarti bahwa objek foo tidak dimodifikasi dengan menambahkan properti padanya atau menghapusnya atau mengubah atribut properti.
  2. Prototipe foo masih merupakan Bar.prototype asli. Ini berarti bahwa prototipe foo tidak berubah menggunakan metode Object.setPrototypeOf() atau dengan menetapkan prototipe baru ke properti _proto_ khusus.
  3. Formulir Bar.prototype berisi getX dan tidak berubah. Yaitu, Bar.prototype tidak diubah dengan menghapus properti, menambahkannya, atau mengubah atributnya.

Dalam kasus umum, ini berarti bahwa kita perlu melakukan 1 pemeriksaan objek itu sendiri, dan 2 pemeriksaan untuk setiap prototipe hingga prototipe yang menyimpan properti yang kita cari. Artinya, Anda perlu melakukan cek 1 + 2N (di mana N adalah jumlah prototipe yang diuji), yang dalam hal ini tidak terlihat begitu buruk, karena rantai prototipe cukup pendek. Namun, mesin sering harus bekerja dengan rantai prototipe yang lebih lama. Ini, misalnya, adalah tipikal dari elemen DOM biasa. Berikut ini sebuah contoh:

 const anchor = document.createElement('a'); // HTMLAnchorElement const title = anchor.getAttribute('title'); 

Di sini kami memiliki HTMLAnchorElement dan kami menyebutnya metode getAttribute() . Rantai prototipe elemen sederhana ini yang mewakili tautan HTML mencakup 6 prototipe! Metode DOM yang paling menarik adalah dalam prototipe HTMLAnchorElement mereka sendiri. Mereka berada di prototipe yang terletak jauh di bawah rantai.
Rantai prototipe

Metode getAttribute() dapat ditemukan di Element.prototype . Ini berarti bahwa setiap kali, ketika metode anchor.getAttribute() , mesin dipaksa untuk melakukan tindakan berikut:

  1. Periksa objek anchor itu sendiri untuk getAttribute .
  2. Memverifikasi bahwa prototipe langsung objek adalah HTMLAnchorElement.prototype .
  3. Mengetahui bahwa HTMLAnchorElement.prototype tidak memiliki metode getAttribute .
  4. Memverifikasi bahwa prototipe berikutnya adalah HTMLElement.prototype .
  5. Mengetahui bahwa tidak ada metode yang diperlukan di sini.
  6. Akhirnya, mengetahui bahwa prototipe berikutnya adalah Element.prototype .
  7. Mencari tahu bahwa ada metode getAttribute .

Seperti yang Anda lihat, 7 pemeriksaan dilakukan di sini. Karena kode seperti itu sangat umum dalam pemrograman web, mesin menggunakan optimisasi untuk mengurangi jumlah pemeriksaan yang diperlukan untuk memuat properti prototipe.

Jika kita kembali ke salah satu contoh sebelumnya, kita dapat mengingat bahwa ketika kita memanggil metode getX dari objek getX , kita melakukan 3 pemeriksaan:

 class Bar {   constructor(x) { this.x = x; }   getX() { return this.x; } } const foo = new Bar(true); const $getX = foo.getX; 

Untuk setiap objek yang ada dalam rantai prototipe, hingga yang berisi properti yang diinginkan, kita perlu memeriksa bentuk objek hanya untuk mengetahui tidak adanya apa yang kita cari. Alangkah baiknya jika kita bisa mengurangi jumlah pemeriksaan dengan mengurangi pemeriksaan prototipe untuk memeriksa ada tidaknya apa yang kita cari. Inilah yang dilakukan oleh mesin dengan langkah sederhana: alih-alih menyimpan tautan prototipe dalam instance itu sendiri, engine menyimpannya dalam bentuk objek.
Penyimpanan referensi prototipe

Setiap formulir memiliki tautan ke sebuah prototipe. Ini juga berarti bahwa setiap kali prototipe foo berubah, mesin bergerak ke bentuk objek yang baru. Sekarang kita hanya perlu memeriksa bentuk objek untuk keberadaan properti di dalamnya dan berhati-hati melindungi tautan prototipe.

Berkat pendekatan ini, kami dapat mengurangi jumlah cek dari 1 + 2N menjadi 1 + N, yang akan mempercepat akses ke properti prototipe. Namun, operasi semacam itu masih sangat intensif sumber daya, karena ada hubungan linier antara jumlah mereka dan panjang rantai prototipe. Mesin telah menerapkan berbagai mekanisme yang bertujuan untuk memastikan bahwa jumlah cek tidak tergantung pada panjang rantai prototipe, dinyatakan sebagai konstan. Ini terutama benar dalam situasi di mana memuat properti yang sama dilakukan beberapa kali.

ValidityCell Property


V8 mengacu pada bentuk prototipe khusus untuk tujuan di atas. Setiap prototipe memiliki bentuk unik yang tidak dibagi dengan objek lain (khususnya, dengan prototipe lain), dan masing-masing bentuk objek prototipe memiliki properti ValidityCell terkait dengannya.
ValidityCell Property

Properti ini dinyatakan tidak valid ketika mengubah prototipe yang terkait dengan formulir, atau prototipe yang ada di atasnya. Pertimbangkan mekanisme ini secara lebih rinci.

Untuk mempercepat operasi berurutan dari memuat properti dari prototipe, V8 menggunakan cache inline yang berisi empat bidang: ValidityCell , Prototype , Shape , Offset .
Bidang cache sebaris

Selama "pemanasan" cache inline saat pertama kali kode dijalankan, V8 mengingat offset di mana properti ditemukan dalam prototipe, prototipe tempat properti ditemukan (dalam contoh ini, Bar.prototype ), bentuk objek ( foo dalam kasus ini) , dan, di samping itu, tautan ke parameter ValidityCell saat ini dari prototipe langsung, tautan yang dalam bentuk objek (dalam hal ini, juga Bar.prototype ).

Lain kali Anda mengakses cache sebaris, mesin harus memeriksa bentuk objek dan ValidityCell . Jika ValidityCell masih valid, mesin dapat langsung mengambil keuntungan dari offset yang disimpan sebelumnya dalam prototipe tanpa melakukan operasi pencarian tambahan.

Ketika prototipe berubah, formulir baru dibuat, dan properti ValidityCell sebelumnya dinyatakan tidak valid. Akibatnya, saat berikutnya Anda mencoba mengakses cache sebaris, itu tidak membawa manfaat apa pun, yang mengarah pada kinerja yang buruk.
Konsekuensi mengubah prototipe

Jika kita kembali ke contoh dengan elemen DOM, ini berarti bahwa setiap perubahan, misalnya, dalam prototipe Object.prototype , tidak hanya akan menyebabkan cache inline untuk Object.prototype itu sendiri, tetapi juga untuk setiap prototipe yang terletak di bawahnya dalam rantai prototipe. termasuk EventTarget.prototype , Node.prototype , Element.prototype , dan seterusnya, langsung ke HTMLAnchorElement.prototype .
Implikasi mengubah Object.prototype

Bahkan, memodifikasi Object.prototype selama eksekusi kode berarti melakukan kerusakan kinerja serius. Jangan lakukan ini.

Kami mempelajari contoh di atas. Misalkan kita memiliki kelas Bar , dan fungsi loadX , yang memanggil metode objek yang dibuat dari kelas Bar . Kami memanggil fungsi loadX beberapa kali, melewati instance dari kelas yang sama.

 function loadX(bar) {   return bar.getX(); // IC  'getX'   `Bar`. } loadX(new Bar(true)); loadX(new Bar(false)); // IC  `loadX`    `ValidityCell`  // `Bar.prototype`. Object.prototype.newMethod = y => y; // `ValidityCell`  IC `loadX`   //    `Object.prototype`  . 

Cache loadX di loadX sekarang menunjuk ke ValidityCell untuk Bar.prototype . , , Object.prototype โ€” JavaScript, ValidityCell , - , .

Object.prototype โ€” , - , . , :

 Object.prototype.foo = function() { /* โ€ฆ */ }; //    : someObject.foo(); //     . delete Object.prototype.foo; 

Object.prototype , - , . , . - , . , ยซ ยป, , .

, , . . Object.prototype , , - .

, โ€” , JS- - , . . , , . , , , .

Ringkasan


, JS- , , , -, ValidityCell , . JavaScript, , ( , , , ).

Pembaca yang budiman! , - , JS, ?

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


All Articles