Ada dua model untuk mengimplementasikan kelas dan instance dalam JavaScript: cara prototyping, dan cara penutupan. Keduanya memiliki kelebihan dan kekurangan, dan ada banyak variasi tambahan. Banyak pemrogram dan perpustakaan memiliki pendekatan dan fungsi utilitas penanganan kelas yang berbeda untuk meliput beberapa bagian bahasa yang lebih buruk.
Hasilnya adalah bahwa di perusahaan campuran Anda akan memiliki mishmash of metaclasses, semua berperilaku sedikit berbeda. Yang lebih buruk, sebagian besar materi tutorial JavaScript mengerikan dan menyajikan semacam kompromi di sela-sela untuk mencakup semua pangkalan, membuat Anda sangat bingung. (Mungkin penulis juga bingung. Model objek JavaScript sangat berbeda dengan kebanyakan bahasa pemrograman, dan di banyak tempat langsung dirancang dengan buruk.)
Mari kita mulai dengan cara prototipe . Ini adalah JavaScript-asli yang paling banyak Anda dapatkan: ada minimum kode overhead dan instanceof akan bekerja dengan instance objek semacam ini.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Kita bisa menambahkan metode ke instance yang dibuat new Shape
dengan menulisnya ke prototype
pencarian fungsi konstruktor ini:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Sekarang untuk subkelasnya, sebanyak yang Anda bisa sebut apa yang JavaScript lakukan subklasifikasi. Kami melakukannya dengan sepenuhnya mengganti prototype
properti sihir aneh itu :
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
sebelum menambahkan metode ke dalamnya:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Contoh ini akan berfungsi dan Anda akan melihat kode seperti itu di banyak tutorial. Tapi man, itu new Shape()
jelek: kami membuat instance kelas dasar meskipun tidak ada Shape yang sebenarnya yang harus dibuat. Itu terjadi untuk bekerja dalam kasus sederhana ini karena JavaScript sangat ceroboh: ini memungkinkan nol argumen untuk diteruskan, dalam hal ini x
dan y
menjadi undefined
dan ditugaskan ke prototipe this.x
dan this.y
. Jika fungsi konstruktor melakukan sesuatu yang lebih rumit, itu akan jatuh datar di wajahnya.
Jadi yang perlu kita lakukan adalah menemukan cara untuk membuat objek prototipe yang berisi metode dan anggota lain yang kita inginkan di tingkat kelas, tanpa memanggil fungsi konstruktor kelas dasar. Untuk melakukan ini, kita harus mulai menulis kode pembantu. Ini adalah pendekatan paling sederhana yang saya tahu:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
Ini mentransfer anggota kelas dasar dalam prototipe ke fungsi konstruktor baru yang tidak melakukan apa-apa, kemudian menggunakan konstruktor itu. Sekarang kita dapat menulis secara sederhana:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
bukannya new Shape()
kesalahan. Kami sekarang memiliki satu set primitif yang dapat diterima untuk membangun kelas.
Ada beberapa penyempurnaan dan ekstensi yang dapat kita pertimbangkan dalam model ini. Sebagai contoh di sini adalah versi sintaksis-gula:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Versi mana pun memiliki kelemahan bahwa fungsi konstruktor tidak dapat diwarisi, seperti dalam banyak bahasa. Jadi, bahkan jika subkelas Anda tidak menambahkan apa pun pada proses konstruksi, ia harus ingat untuk memanggil konstruktor dasar dengan argumen apa pun yang diinginkan dasar. Ini bisa sedikit otomatis menggunakan apply
, tetapi Anda masih harus menulis:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Jadi ekstensi yang umum adalah untuk membagi hal-hal inisialisasi ke dalam fungsinya sendiri daripada konstruktor itu sendiri. Fungsi ini kemudian dapat mewarisi dari basis dengan baik:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Sekarang kita baru saja mendapatkan fungsi konstruktor boilerplate yang sama untuk setiap kelas. Mungkin kita bisa memindahkannya ke dalam fungsi penolongnya sendiri sehingga kita tidak harus terus mengetiknya, misalnya alih-alih Function.prototype.subclass
, memutarnya dan membiarkan Function kelas dasar mengeluarkan subclass:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... yang mulai sedikit lebih mirip dengan bahasa lain, walaupun dengan sintaksis yang sedikit klumsier. Anda dapat menaburkan beberapa fitur tambahan jika suka. Mungkin Anda ingin makeSubclass
mengambil dan mengingat nama kelas dan memberikan default toString
menggunakannya. Mungkin Anda ingin membuat konstruktor mendeteksi ketika secara tidak sengaja dipanggil tanpa new
operator (yang sebaliknya akan sering mengakibatkan debugging yang sangat menjengkelkan):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Mungkin Anda ingin memasukkan semua anggota baru dan makeSubclass
menambahkannya ke prototipe, agar Anda tidak perlu menulis Class.prototype...
terlalu banyak. Banyak sistem kelas melakukan itu, misalnya:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Ada banyak fitur potensial yang mungkin Anda anggap diinginkan dalam sistem objek dan tidak ada yang benar-benar setuju pada satu formula tertentu.
Cara penutupan , kalau begitu. Ini menghindari masalah warisan berbasis prototipe JavaScript, dengan tidak menggunakan warisan sama sekali. Sebagai gantinya:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Sekarang setiap instance Shape
akan memiliki salinan toString
metode sendiri (dan metode lain atau anggota kelas lain yang kami tambahkan).
Hal buruk dari setiap instance memiliki salinannya sendiri dari setiap anggota kelas adalah bahwa itu kurang efisien. Jika Anda berurusan dengan banyak contoh subclass, pewarisan prototipikal dapat membantu Anda dengan lebih baik. Juga memanggil metode kelas dasar sedikit mengganggu seperti yang Anda lihat: kita harus ingat apa metode itu sebelum konstruktor subkelas menimpa, atau hilang.
[Juga karena tidak ada warisan di sini, instanceof
operator tidak akan bekerja; Anda harus menyediakan mekanisme Anda sendiri untuk mengendus kelas jika Anda membutuhkannya. Sementara Anda bisa mengutak-atik objek prototipe dengan cara yang sama seperti dengan warisan prototipe, ini agak rumit dan tidak terlalu layak hanya untuk instanceof
bekerja.]
Hal yang baik tentang setiap instance yang memiliki metode sendiri adalah bahwa metode tersebut kemudian dapat terikat pada instance spesifik yang memilikinya. Ini berguna karena cara aneh JavaScript this
dalam mengikat pemanggilan metode, yang memiliki kesimpulan bahwa jika Anda melepaskan metode dari pemiliknya:
var ts= mycircle.toString;
alert(ts());
kemudian this
di dalam metode tidak akan menjadi contoh Circle seperti yang diharapkan (itu sebenarnya akan menjadi window
objek global , menyebabkan celaka debugging luas). Pada kenyataannya ini biasanya terjadi ketika suatu metode diambil dan ditugaskan ke setTimeout
, onclick
atau EventListener
secara umum.
Dengan cara prototipe, Anda harus menyertakan penutupan untuk setiap tugas seperti:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
atau, di masa depan (atau sekarang jika Anda meretas Function.prototype) Anda juga dapat melakukannya dengan function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
jika instans Anda dilakukan dengan cara penutupan, pengikatan dilakukan secara gratis oleh penutupan atas variabel instan (biasanya disebut that
atau self
, meskipun secara pribadi saya akan menyarankan agar yang terakhir tidak self
memiliki arti lain yang berbeda dalam JavaScript). Anda tidak mendapatkan argumen 1, 1
dalam cuplikan di atas secara gratis, jadi Anda masih perlu penutupan lain atau bind()
jika Anda perlu melakukannya.
Ada banyak varian pada metode penutupan juga. Anda mungkin lebih suka menghilangkan this
sepenuhnya, membuat yang baru that
dan mengembalikannya daripada menggunakan new
operator:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Jalan mana yang “tepat”? Kedua. Mana yang "terbaik"? Itu tergantung situasi Anda. FWIW Saya cenderung membuat prototipe untuk warisan JavaScript nyata ketika saya melakukan banyak hal OO, dan penutupan untuk efek halaman sekali pakai yang sederhana.
Namun kedua cara tersebut cukup kontra-intuitif bagi kebanyakan programmer. Keduanya memiliki banyak variasi yang berpotensi berantakan. Anda akan bertemu keduanya (dan juga banyak skema di antara dan umumnya rusak) jika Anda menggunakan kode / pustaka orang lain. Tidak ada satu jawaban yang diterima secara umum. Selamat datang di dunia objek JavaScript yang luar biasa.
[Ini telah menjadi bagian 94 dari Mengapa JavaScript Bukan Bahasa Pemrograman Favorit Saya.]