Cara kerja JS: kelas dan pewarisan, transpilasi dalam Babel dan TypeScript

Kelas adalah salah satu cara paling populer untuk menyusun proyek perangkat lunak dewasa ini. Pendekatan pemrograman ini juga digunakan dalam JavaScript. Hari ini kami menerbitkan terjemahan bagian 15 dari seri ekosistem JS. Artikel ini akan membahas berbagai pendekatan untuk mengimplementasikan kelas dalam JavaScript, mekanisme pewarisan, dan transpirasi. Kami akan mulai dengan memberi tahu Anda bagaimana prototipe bekerja dan dengan menganalisis berbagai cara untuk mensimulasikan pewarisan berbasis kelas di perpustakaan populer. Selanjutnya, kita akan berbicara tentang bagaimana, berkat transpilasi, dimungkinkan untuk menulis program JS yang menggunakan fitur yang tidak tersedia dalam bahasa atau, meskipun mereka ada dalam bentuk standar baru atau proposal yang berada pada tahap persetujuan yang berbeda, belum dilaksanakan di JS- mesin. Secara khusus, kita akan berbicara tentang Babel dan TypeScript dan kelas ECMAScript 2015. Setelah itu, kita akan melihat beberapa contoh yang menunjukkan fitur implementasi internal kelas di mesin V8 JS.
gambar


Ulasan


Dalam JavaScript, kami terus-menerus dihadapkan dengan objek, bahkan ketika tampaknya kami bekerja dengan tipe data primitif. Misalnya, buat string literal:

const name = "SessionStack"; 

Setelah itu, kita dapat langsung beralih ke name untuk memanggil berbagai metode objek bertipe String , yang string literal yang kita buat akan secara otomatis dikonversi.

 console.log(name.repeat(2)); // SessionStackSessionStack console.log(name.toLowerCase()); // sessionstack 

Tidak seperti bahasa lain, dalam JavaScript, setelah membuat variabel yang berisi, misalnya, string atau angka, kita dapat, tanpa melakukan konversi eksplisit, bekerja dengan variabel ini seolah-olah awalnya dibuat menggunakan kata kunci new dan konstruktor yang sesuai. Akibatnya, karena penciptaan objek secara otomatis yang merangkum nilai-nilai primitif, Anda dapat bekerja dengan nilai-nilai seperti jika mereka objek, khususnya, merujuk pada metode dan properti mereka.

Fakta penting lainnya mengenai sistem tipe JavaScript adalah bahwa, misalnya, array juga objek. Jika Anda melihat output dari perintah typeof untuk array, Anda dapat melihat bahwa itu melaporkan bahwa entitas yang diselidiki memiliki tipe data object . Akibatnya, ternyata indeks elemen-elemen array hanya properti dari objek tertentu. Oleh karena itu, ketika kita mengakses elemen array dengan indeks, ia akan bekerja dengan properti objek bertipe Array dan mendapatkan nilai properti ini. Jika kita berbicara tentang bagaimana data disimpan di dalam objek dan array biasa, maka dua konstruksi berikut mengarah pada pembuatan struktur data yang hampir identik:

 let names = ["SessionStack"]; let names = { "0": "SessionStack", "length": 1 } 

Akibatnya, akses ke elemen-elemen array dan ke properti objek dilakukan pada kecepatan yang sama. Penulis artikel ini mengatakan bahwa ia menemukan jalan untuk menyelesaikan satu masalah kompleks. Yaitu, begitu ia perlu melakukan optimasi serius terhadap sepotong kode yang sangat penting dalam proyek tersebut. Setelah mencoba banyak pendekatan sederhana, ia memutuskan untuk mengganti semua objek yang digunakan dalam kode ini dengan array. Secara teori, mengakses elemen array lebih cepat daripada bekerja dengan kunci tabel hash. Yang mengejutkan, penggantian ini tidak mempengaruhi kinerja dengan cara apa pun, karena bekerja dengan array dan bekerja dengan objek dalam JavaScript turun untuk berinteraksi dengan kunci-kunci tabel hash, yang, dalam kedua kasus, membutuhkan jumlah waktu yang sama.

Mensimulasikan kelas menggunakan prototipe


Ketika kita berpikir tentang objek, hal pertama yang terlintas dalam pikiran adalah kelas. Mungkin masing-masing dari mereka yang terlibat dalam pemrograman hari ini menciptakan aplikasi yang strukturnya didasarkan pada kelas dan pada hubungan di antara mereka. Meskipun objek dalam JavaScript dapat ditemukan secara harfiah di mana-mana, bahasanya tidak menggunakan sistem pewarisan berbasis kelas tradisional. JavaScript menggunakan prototipe untuk memecahkan masalah serupa.


Obyek dan prototipe-nya

Dalam JavaScript, setiap objek dikaitkan dengan objek lain - dengan prototipe sendiri. Ketika Anda mencoba mengakses properti atau metode objek, pencarian apa yang Anda butuhkan pertama kali dilakukan di objek itu sendiri. Jika pencarian tidak berhasil, ia melanjutkan prototipe objek.

Pertimbangkan contoh sederhana yang menjelaskan fungsi konstruktor untuk kelas dasar Component :

 function Component(content) { this.content = content; } Component.prototype.render = function() {   console.log(this.content); } 

Di sini kita menetapkan fungsi render() ke metode prototipe, karena kita membutuhkan setiap instance dari kelas Component untuk menggunakan metode ini. Ketika, dalam setiap instance dari Component , metode render dipanggil, pencariannya dimulai pada objek itu sendiri yang dipanggil. Kemudian pencarian berlanjut di prototipe, di mana sistem menemukan metode ini.


Prototipe dan dua contoh kelas Komponen

Sekarang mari kita coba memperluas kelas Component . Mari kita buat konstruktor untuk kelas baru - InputField :

 function InputField(value) {   this.content = `<input type="text" value="${value}" />`; } 

Jika kita membutuhkan kelas InputField memperluas fungsionalitas kelas Component dan dapat memanggil metode InputField , kita perlu mengubah prototipe. Ketika suatu metode dipanggil pada turunan kelas anak, tidak masuk akal untuk mencarinya dalam prototipe kosong. Kita perlu, dalam pencarian metode ini, ditemukan di kelas Component . Karena itu, kita perlu melakukan hal berikut:

 InputField.prototype = Object.create(new Component()); 

Sekarang, ketika bekerja dengan turunan dari kelas InputField dan memanggil metode kelas Component , metode ini akan ditemukan dalam prototipe kelas Component . Untuk menerapkan sistem warisan, Anda harus menghubungkan prototipe InputField ke turunan dari kelas Component . Banyak perpustakaan menggunakan Object.setPrototypeOf () untuk menyelesaikan masalah ini.


Memperluas Kelas Komponen dengan Kelas InputField

Namun, tindakan di atas tidak cukup untuk menerapkan mekanisme yang mirip dengan warisan tradisional. Setiap kali kami memperluas kelas, kami perlu melakukan tindakan berikut:

  • Jadikan prototipe kelas turunan sebagai turunan dari kelas induk.
  • Panggil, di konstruktor kelas turunan, konstruktor kelas induk untuk memastikan bahwa kelas induk diinisialisasi dengan benar.
  • Berikan mekanisme untuk memanggil metode dari kelas induk dalam situasi di mana kelas turunan menimpa metode induk, tetapi ada kebutuhan untuk memanggil implementasi asli dari metode ini dari kelas induk.

Seperti yang Anda lihat, jika pengembang JS ingin menggunakan kemampuan warisan berbasis kelas, ia harus terus melakukan langkah-langkah di atas. Jika Anda perlu membuat banyak kelas, semua ini bisa dibuat dalam bentuk fungsi yang cocok untuk digunakan kembali.

Bahkan, tugas mengatur warisan berdasarkan kelas awalnya diselesaikan dalam praktik pengembangan JS dengan cara ini. Secara khusus, menggunakan berbagai perpustakaan. Solusi semacam itu menjadi sangat populer, yang dengan jelas menunjukkan bahwa ada sesuatu yang jelas hilang dalam JavaScript. Itulah sebabnya ECMAScript 2015 memperkenalkan konstruksi sintaksis baru yang bertujuan mendukung pekerjaan dengan kelas dan menerapkan mekanisme pewarisan yang sesuai.

Transilasi kelas


Setelah fitur baru ECMAScript 2015 (ES6) diusulkan, komunitas JS ingin memanfaatkannya sesegera mungkin, tanpa menunggu penyelesaian proses panjang untuk menambahkan dukungan untuk fitur-fitur ini di mesin dan browser JS. Dalam memecahkan masalah seperti itu, transpilasi itu baik. Dalam hal ini, kompilasi dikurangi menjadi mentransformasikan kode JS yang ditulis sesuai dengan aturan ES6 ke tampilan yang dapat dimengerti oleh browser yang sejauh ini tidak mendukung kemampuan ES6. Sebagai hasilnya, misalnya, dimungkinkan untuk mendeklarasikan kelas dan menerapkan mekanisme pewarisan berbasis kelas sesuai dengan aturan ES6 dan mengubah konstruksi ini menjadi kode yang berfungsi di browser apa pun. Secara skematis, proses ini, menggunakan contoh memproses fungsi panah oleh transpiler (fitur bahasa baru lain yang membutuhkan waktu untuk mendukung), dapat direpresentasikan seperti yang ditunjukkan pada gambar di bawah ini.


Transpilasi

Salah satu pengalih JavaScript yang paling populer adalah Babel.js. Mari kita lihat cara kerjanya dengan melakukan kompilasi kode deklarasi kelas Component , yang kita bicarakan di atas. Jadi di sini adalah kode ES6:

 class Component { constructor(content) {   this.content = content; } render() { console.log(this.content) } } const component = new Component('SessionStack'); component.render(); 

Dan inilah kode ini setelah transformasi:

 var Component = function () { function Component(content) {   _classCallCheck(this, Component);   this.content = content; } _createClass(Component, [{   key: 'render',   value: function render() {     console.log(this.content);   } }]); return Component; }(); 

Seperti yang Anda lihat, ECMAScript 5-code diperoleh pada output transpiler, yang dapat dijalankan di lingkungan apa pun. Selain itu, panggilan ke beberapa fungsi yang merupakan bagian dari perpustakaan standar Babel ditambahkan di sini.

Kita berbicara tentang fungsi _classCallCheck() dan _createClass() termasuk dalam kode yang ditranskrip. Fungsi pertama, _classCallCheck() , dirancang untuk mencegah fungsi konstruktor dipanggil seperti fungsi biasa. Untuk melakukan ini, ia memeriksa apakah konteks di mana fungsi dipanggil adalah konteks instance dari kelas Component . Kode memeriksa untuk melihat apakah kata kunci ini menunjuk ke contoh yang sama. Fungsi kedua, _createClass() , membuat properti objek yang diteruskan kepadanya sebagai array objek yang berisi kunci dan nilainya.

Untuk memahami cara kerja pewarisan, kami menganalisis kelas InputField , yang merupakan turunan dari kelas Component . Inilah cara hubungan kelas bersatu dalam ES6:

 class InputField extends Component {   constructor(value) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Berikut adalah hasil pengubahan kode ini menggunakan Babel:

 var InputField = function (_Component) { _inherits(InputField, _Component); function InputField(value) {   _classCallCheck(this, InputField);   var content = '<input type="text" value="' + value + '" />';   return _possibleConstructorReturn(this, (InputField.__proto__ || Object.getPrototypeOf(InputField)).call(this, content)); } return InputField; }(Component); 

Dalam contoh ini, logika mekanisme pewarisan dienkapsulasi dalam panggilan ke fungsi _inherits() . Itu melakukan tindakan yang sama yang kami jelaskan di atas, terkait, khususnya, dengan menulis ke prototipe kelas turunan dari kelas induk.

Untuk mengubah kode, Babel melakukan beberapa transformasi. Pertama, kode ES6 diuraikan dan dikonversi ke representasi perantara yang disebut pohon sintaksis abstrak . Kemudian pohon sintaksis abstrak yang dihasilkan dikonversi ke pohon lain, setiap node yang ditransformasikan menjadi setara ES5. Akibatnya, pohon ini dikonversi ke kode JS.

Pohon sintaksis abstrak di Babel


Pohon sintaksis abstrak berisi node, yang masing-masing hanya memiliki satu simpul induk. Babel memiliki tipe dasar untuk node. Ini berisi informasi tentang apa simpul itu dan di mana ia dapat ditemukan dalam kode. Ada berbagai jenis node, misalnya, node untuk mewakili literal, seperti string, angka, nilai null , dan sebagainya. Selain itu, ada node untuk mewakili ekspresi yang digunakan untuk mengontrol aliran eksekusi program ( if membangun), dan node untuk loop ( for , while ). Ada juga tipe simpul khusus untuk mewakili kelas. Ini adalah turunan dari kelas dasar Node . Dia memperluas kelas ini dengan menambahkan bidang untuk menyimpan referensi ke kelas dasar dan ke tubuh kelas sebagai simpul yang terpisah.
Ubah fragmen kode berikut ke struktur sintaksis abstrak:

 class Component { constructor(content) {   this.content = content; } render() {   console.log(this.content) } } 

Inilah tampilan representasi skematisnya.


Pohon sintaksis abstrak

Setelah membuat pohon, masing-masing node ditransformasikan menjadi simpul ES5 yang sesuai, setelah itu pohon baru ini dikonversi menjadi kode yang sesuai dengan standar ECMAScript 5. Selama proses konversi, pertama-tama cari node yang letaknya paling jauh dari node root, setelah itu node ini dikonversi ke kode menggunakan potongan yang dihasilkan untuk setiap node. Setelah itu, proses diulang. Teknik ini disebut pencarian dalam .

Dalam contoh di atas, kode untuk dua node MethodDefinition akan dihasilkan terlebih dahulu, setelah itu kode untuk node ClassBody akan dihasilkan, dan akhirnya, kode untuk node ClassDeclaration .

Transparansi TypeScript


Sistem populer lain yang menggunakan transpilasi adalah TypeScript. Ini adalah bahasa pemrograman yang kodenya ditransformasikan menjadi kode ECMAScript 5 yang dapat dimengerti oleh mesin JS. Ini menawarkan sintaks baru untuk menulis aplikasi JS. Berikut cara menerapkan kelas Component pada TypeScript:

 class Component {   content: string;   constructor(content: string) {       this.content = content;   }   render() {       console.log(this.content)   } } 

Berikut adalah sintaksis abstrak untuk kode ini.


Pohon sintaksis abstrak

TypeScript mendukung warisan.

 class InputField extends Component {   constructor(value: string) {       const content = `<input type="text" value="${value}" />`;       super(content);   } } 

Berikut ini adalah hasil dari transpilasi kode ini:

 var InputField = /** @class */ (function (_super) {   __extends(InputField, _super);   function InputField(value) {       var _this = this;       var content = "<input type=\"text\" value=\"" + value + "\" />";       _this = _super.call(this, content) || this;       return _this;   }   return InputField; }(Component)); 

Seperti yang Anda lihat, ini lagi-lagi kode ES5, di mana, di samping konstruksi standar, ada panggilan ke beberapa fungsi dari pustaka TypeScript. Kemampuan fungsi __extends() mirip dengan yang kami bicarakan di awal materi ini.

Berkat adopsi Babel dan TypeScript yang meluas, mekanisme untuk mendeklarasikan kelas dan mengatur warisan berbasis kelas telah menjadi alat standar untuk menyusun aplikasi JS. Ini berkontribusi pada penambahan dukungan untuk mekanisme ini di browser.

Dukungan Kelas Peramban


Dukungan kelas muncul di browser Chrome pada 2014. Ini memungkinkan browser untuk bekerja dengan deklarasi kelas tanpa menggunakan transpilasi atau pustaka bantu apa pun.


Bekerja dengan kelas di konsol Chrome JS

Faktanya, dukungan browser untuk mekanisme ini tidak lebih dari gula sintaksis. Konstruksi ini dikonversi ke struktur dasar yang sama yang sudah didukung oleh bahasa. Akibatnya, bahkan jika Anda menggunakan sintaks baru, pada level yang lebih rendah, semuanya akan terlihat seperti membuat konstruktor dan memanipulasi prototipe objek:


Dukungan kelas adalah gula sintaksis

Dukungan Kelas di V8


Mari kita bicara tentang bagaimana dukungan kelas ES6 bekerja di mesin V8 JS. Dalam materi sebelumnya yang dikhususkan untuk pohon sintaksis abstrak, kami berbicara tentang fakta bahwa ketika mempersiapkan JS-code untuk dieksekusi, sistem mem-parsingnya dan membentuk pohon sintaksis abstrak pada dasarnya. Ketika parsing konstruksi deklarasi kelas, node tipe ClassLiteral jatuh ke pohon sintaksis abstrak.

Node-node ini menyimpan beberapa hal menarik. Pertama, itu adalah konstruktor sebagai fungsi terpisah, dan kedua, itu adalah daftar properti kelas. Ini bisa berupa metode, getter, setter, bidang publik atau pribadi. Node seperti itu, di samping itu, menyimpan referensi ke kelas induk, yang memperluas kelas yang membentuk simpul, yang, sekali lagi, menyimpan konstruktor, daftar properti dan tautan ke kelas induknya sendiri.

Setelah node ClassLiteral baru ditransformasikan menjadi kode , itu dikonversi menjadi konstruksi yang terdiri dari fungsi dan prototipe.

Ringkasan


Penulis materi ini mengatakan bahwa SessionStack berusaha untuk mengoptimalkan kode perpustakaannya semaksimal mungkin, karena harus menyelesaikan tugas-tugas sulit mengumpulkan informasi tentang segala sesuatu yang terjadi di halaman web. Dalam menyelesaikan masalah ini, perpustakaan seharusnya tidak memperlambat pekerjaan halaman yang dianalisis. Optimalisasi tingkat ini memerlukan memperhitungkan detail terkecil dari ekosistem JavaScript yang memengaruhi kinerja, khususnya, dengan mempertimbangkan fitur bagaimana kelas dan mekanisme pewarisan diatur dalam ES6.

Pembaca yang budiman! Apakah Anda menggunakan konstruksi sintaks ES6 untuk bekerja dengan kelas dalam JavaScript?

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


All Articles