Mendefinisikan Kelas atau Objek ECMAScript

Menggunakan objek yang diatur sebelumnya hanya sebahagian daripada keupayaan bahasa berorientasikan objek, kekuatan sebenarnya adalah untuk dapat membuat kelas dan objek khas sendiri.

ECMAScript mempunyai banyak cara untuk membuat objek atau kelas.

Cara pabrik

Cara asal

Karena properti objek boleh ditakrifkan secara dinamik selepas objek dibuat, ramai pengembang menulis kod seperti yang berikut semasa JavaScript diserahkan untuk pertama kalinya:

var oCar = new Object;
oCar.color = "blue";
oCar.doors = 4;
oCar.mpg = 25;
oCar.showColor = function() {
  alert(this.color);
};

TIY

Dalam kode di atas, objek car dibuat. Lalu diatur beberapa propertinya: warnanya biru, empat pintu, dan setiap liter minyak dapat berjalan 25 mil. Properti terakhir adalah penunjuk ke fungsi, yang berarti properti ini adalah method. Setelah menjalankan kode ini, objek car dapat digunakan.

Namun ada masalah di sini, yaitu mungkin perlu untuk membuat beberapa instansi car.

Solusi: Cara pabrik

Untuk memecahkan masalah ini, para pengembang menciptakan fungsi pabrik yang dapat membuat dan mengembalikan objek jenis khusus (factory function).

Contohnya, fungsi createCar() dapat digunakan untuk mengembalikan operasi pembuatan objek car yang terdaftar sebelumnya:

function createCar() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar();
var oCar2 = createCar();

TIY

Di sini, semua kode dalam contoh pertama termasuk di dalam fungsi createCar(). Selain itu, ada satu baris kode ekstra, yang mengembalikan objek car (oTempCar) sebagai nilai fungsi. Menjalankan fungsi ini, akan membuat objek baru dan memberikannya semua properti yang diperlukan, menyalin objek car yang dijelaskan sebelumnya. Oleh cara ini, kami dapat dengan mudah membuat dua versi objek car (oCar1 dan oCar2), yang propertinya sama sekali.

Mengirimkan parameter ke fungsi

Kami juga dapat mengubah fungsi createCar(), untuk memasukkan nilai standar setiap properti, bukannya hanya memberikan nilai standar properti:

function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//keluar "red"
oCar2.showColor();		//keluar "blue"

TIY

Dengan menambahkan parameter ke fungsi createCar(), dapat memberikan nilai properti color, doors dan mpg objek car yang akan dibuat. Ini membuat dua objek memiliki properti yang sama, tetapi nilai properti yang berbeda.

Mengdefinikan method objek di luar fungsi pabrik

Walaupun ECMAScript semakin resmi, tetapi cara membuat objek masih diabaikan, dan standarisasi ini masih disambut dengan lawan hingga kini. Sebagian disebabkan oleh alasan semantik (kelihatannya tidak seperti yang digunakan dengan operator new konstruktur), sebagian lagi disebabkan oleh alasan fungsional. Alasan fungsional berada di tempat harus dibuat objek. Dalam contoh sebelumnya, setiap kali dipanggil fungsi createCar(), akan dibuat fungsi baru showColor(), yang berarti setiap objek memiliki versi showColor() sendiri. Tetapi sebenarnya, setiap objek membagi fungsi yang sama.

Beberapa pengembang mendefinisikan metode objek di luar fungsi pabrik, lalu mengarahkan ke metode ini melalui properti, untuk menghindari masalah ini:}}

function showColor() {
  alert(this.color);
}
function createCar(sColor,iDoors,iMpg) {
  var oTempCar = new Object;
  oTempCar.color = sColor;
  oTempCar.doors = iDoors;
  oTempCar.mpg = iMpg;
  oTempCar.showColor = showColor;
  return oTempCar;
}
var oCar1 = createCar("red",4,23);
var oCar2 = createCar("blue",3,25);
oCar1.showColor();		//keluar "red"
oCar2.showColor();		//keluar "blue"

TIY

Dalam kode yang diubah di atas, fungsi showColor() didefinisikan sebelum fungsi createCar(). Dalam createCar(), diberikan pointer kepada fungsi showColor() yang sudah ada kepada objek. Dalam hal fungsional, hal ini mengatasi masalah pembuatan kembali objek fungsi; tetapi dalam hal semantik, fungsi ini kurang seperti metode objek.

Masalah-masalah ini semua memicudidefinisikan pengembangmunculnya konstraktor.

Cara konstraktor

Membuat konstraktor seperti membuatan fungsi pabrik sangat mudah. Langkah pertama adalah memilih nama kelas, yaitu nama konstraktor. Menurut konvensi, huruf pertama nama ini diukur, untuk membedakannya dengan nama variabel yang biasanya kecil. Selain hal ini, konstraktor menunjukkan seperti fungsi pabrik. Berhati-hatilah contoh di bawah ini:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.showColor = function() {
    alert(this.color);
  };
}
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);

TIY

Di bawah ini kami akan menggambarkan perbezaan kod di atas dengan cara pabrik. Pada mulanya, objek tidak dibuat di dalam konstraktor, melainkan menggunakan kata kunci this. Saat menggunakan operator new untuk konstraktor, objek akan dibuat terlebih dahulu sebelum baris pertama dijalankan, hanya dengan cara this yang dapat mengakses objek itu. kemudian dapat langsung memberikan nilai kepada properti this, secara default adalah nilai kembalian konstraktor (tidak perlu secara eksplisit menggunakan operator return).

Sekarang, penggunaan operator new dan nama kelas Car untuk menciptakan objek lebih seperti cara penciptaan objek umum dalam ECMAScript.

Anda mungkin akan bertanya, apakah cara ini ada masalah yang sama dalam pengelolaan fungsi seperti yang ada di cara sebelumnya? Ya.

Seperti fungsi pabrik, fungsi konstruktur akan menghasilkan ulang fungsi, menciptakan versi fungsi yang berbeda untuk setiap objek. Meskipun mirip dengan fungsi pabrik, fungsi konstruktur juga dapat diulang dengan fungsi luar, tetapi secara semantik ini tidak mempunyai makna. Ini adalah keunggulan cara prototipe yang akan disebutkan dalam hal ini.

Cara prototipe

Cara prototipe memakai atribut prototype objek, dapat dianggap seperti prototype yang dipakai untuk menciptakan objek baru.

Di sini, pertama-tama digunakan konstruktur kosong untuk menetapkan nama kelas. kemudian semua atribut dan metode langsung diberikan ke atribut prototype. Kita menulis ulang contoh sebelumnya, kode seperti berikut:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();

TIY

Pada kode ini, pertama-tama definisikan fungsi konstruktur (Car), tanpa ada kode di dalamnya. Berikutnya, beberapa baris kode menambahkan atribut ke atribut prototype untuk mendefinisikan atribut objek Car. Pada saat pemanggilan new Car(), semua atribut prototype segera diberikan kepada objek yang akan dibuat, yang berarti semua instansia Car menyimpan pointer ke fungsi showColor(). Dalam arti semantik, semua atribut kelihat seperti milik satu objek, sehingga memecahkan masalah yang ada di kedua cara sebelumnya.

Selain itu, dengan cara ini, anda dapat menggunakan operator instanceof untuk memeriksa tipe objek yang ditunjuk variabel. Oleh karena itu, kode di bawah ini akan keluar TRUE:

alert(oCar1 instanceof Car);	// keluaran "true"

Masalah cara prototipe

Cara prototipe terlihat seperti solusi yang bagus. Sayangnya, ini belum sepenuhnya memuaskan.

Pada awalnya, fungsi konstruktur ini tidak mempunyai parameter. Dengan cara prototipe, nilai atribut yang diinisialisasi tidak dapat disetel melalui penyerahan parameter kepada fungsi konstruktur, karena atribut color untuk Car1 dan Car2 sama-sama "blue", atribut doors sama-sama 4, dan atribut mpg sama-sama 25. Ini berarti atribut harus diubah setelah objek dibuat, hal ini sangat mengecewakan, tetapi ini belum selesai. Masalah yang sebenarnya muncul saat atribut menunjuk kepada objek, bukannya fungsi. Dengan cara ini, peng共享 fungsi tidak akan menyebabkan masalah, tetapi objek jarang diungkapkan ke berbagai instansia. Berpikir tentang contoh di bawah ini:

function Car() {
}
Car.prototype.color = "blue";
Car.prototype.doors = 4;
Car.prototype.mpg = 25;
Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car();
var oCar2 = new Car();
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Paparkan "Mike,John,Bill"
alert(oCar2.drivers);	//Paparkan "Mike,John,Bill"

TIY

Dalam kode di atas, properti drivers adalah penunjuk ke objek Array, yang mengandung dua nama "Mike" dan "John". Karena drivers adalah nilai referensi, dua instansia Car mengarah ke array yang sama. Ini berarti menambah nilai "Bill" ke drivers oCar1, nilai ini juga dapat dilihat di drivers oCar2. Menampilkan salah satu penunjuk ini, hasilnya adalah menampilkan string "Mike,John,Bill".

Karena ada banyak pertanyaan tentang penciptaan objek, Anda mungkin berpikir apakah ada cara yang masuk akal untuk mencipta objek. Jawabannya adalah ya, dan hal ini memerlukan penggunaan kombinasi konstraktor dan cara prototipe.

Kombinasi konstraktor/prototipe

Kombinasi penggunaan konstraktor dan cara prototipe dapat mencipta objek seperti dalam bahasa pemrograman lain. Konsep ini sangat sederhana, yaitu menggunakan konstraktor untuk mendefinisikan semua properti bukan fungsi objek, dan menggunakan cara prototipe untuk mendefinisikan properti fungsi (metode) objek. Akibatnya, semua fungsi hanya di buat sekali, dan setiap objek memiliki instansia properti objeknya sendiri.

Kami telah menulis ulang contoh sebelumnya, kode seperti berikut:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
}
Car.prototype.showColor = function() {
  alert(this.color);
};
var oCar1 = new Car("red",4,23);
var oCar2 = new Car("blue",3,25);
oCar1.drivers.push("Bill");
alert(oCar1.drivers);	//Paparkan "Mike,John,Bill"
alert(oCar2.drivers);	//Paparkan "Mike,John"

TIY

Sekarang ia lebih seperti untuk mencipta objek biasa. Semua properti bukan fungsi di ciptakan di dalam konstraktor, yang berarti kembali dapat memberikan nilai baku properti melalui parameter konstraktor. Karena hanya menciptakan satu instansia fungsi showColor(), jadi tidak ada pengeluaran memori. Selain itu, menambah nilai "Bill" ke dalam array drivers oCar1, tidak akan mempengaruhi array oCar2, jadi saat menampilkan nilai array ini, oCar1.drivers menampilkan "Mike,John,Bill", sementara oCar2.drivers menampilkan "Mike,John". Karena digunakan cara prototipe, jadi masih dapat menggunakan operator instanceof untuk menentukan tipe objek.

这种方式是 ECMAScript 采用的主要方式,它具有其他方式的特性,却没有他们的副作用。不过,有些开发者仍觉得这种方法不够完美。

动态原型方法

对于习惯使用其他语言的开发者来说,使用混合的构造函数/原型方式感觉不那么和谐。毕竟,定义类时,大多数面向对象语言都对属性和方法进行了视觉上的封装。请考虑下面的 Java 类:

class Car {
  public String color = "blue";
  public int doors = 4;
  public int mpg = 25;
  public Car(String color, int doors, int mpg) {
    this.color = color;
    this.doors = doors;
    this.mpg = mpg;
  }
  public void showColor() {
    System.out.println(color);
  }
}

Java 很好地打包了 Car 类的所有属性和方法,因此看见这段代码就知道它要实现什么功能,它定义了一个对象的信息。批评混合的构造函数/原型方式的人认为,在构造函数内部找属性,在其外部找方法的做法不合逻辑。因此,他们设计了动态原型方法,以提供更友好的编码风格。

动态原型方法的基本思想与混合的构造函数/原型方式相同,即在构造函数内定义非函数属性,而函数属性则利用原型属性定义。唯一的区别是赋予对象方法的位置。下面是用动态原型方法重写的 Car 类:

function Car(sColor,iDoors,iMpg) {
  this.color = sColor;
  this.doors = iDoors;
  this.mpg = iMpg;
  this.drivers = new Array("Mike","John");
  if (typeof Car._initialized == "undefined"}) {
    Car.prototype.showColor = function() {
      alert(this.color);
    };
    Car._initialized = true;
  }
}

TIY

Fungsi konstruktur ini belum berubah sampai dieksekusi pengecekan typeof Car._initialized sama dengan "undefined". Ini adalah bagian paling penting dari metode prototype dinamis. Jika nilai ini belum ditentukan, fungsi konstruktur akan melanjutkan mendefinisikan metode objek dengan cara prototype, lalu mengatur Car._initialized menjadi true. Jika nilai ini sudah ditentukan (nilai true saat typeof adalah Boolean), maka metode ini tidak akan dibuat lagi. Dalam arti yang singkat, metode ini menggunakan tanda ( _initialized ) untuk mengecek apakah sudah ada metode yang diberikan ke prototype. Metode ini hanya dibuat dan diatur sekali, para pengembang OOP tradisional akan menemukan bahwa kode ini seperti definisi kelas di bahasa lain.

Cara factory campur

Cara ini biasanya digunakan sebagai alternatif ketika cara yang sebelumnya tidak dapat digunakan. Tujuannya adalah untuk membuat konstruktur palsu, hanya untuk mengembalikan instansi objek lain.

Kode ini terlihat seperti factory function:

function Car() {
  var oTempCar = new Object;
  oTempCar.color = "blue";
  oTempCar.doors = 4;
  oTempCar.mpg = 25;
  oTempCar.showColor = function() {
    alert(this.color);
  };
  return oTempCar;
}

TIY

Berbeda dengan cara klasik, cara ini menggunakan operator new, sehingga tampak seperti fungsi konstruktur yang sebenarnya:

var car = new Car();

Karena di dalam fungsi konstruktur Car() digunakan operator new, operator new kedua (yang berada di luar fungsi konstruktur) akan diabaikan, objek yang dibuat di dalam fungsi konstruktur dikirim kembali ke variabel car.

Cara ini memiliki masalah yang sama dalam manajemen internal metode objek seperti cara klasik. Saya menyarankan: kecuali untuk kepentingan yang mendesak, selamat untuk menghindari cara ini.

Pilih cara mana

Seperti yang disebutkan sebelumnya, yang paling umum digunakan saat ini adalah campuran konstruktur/fungsi prototype. Selain itu, metode primitif dinamis sangat populer, yang secara fungsi setara dengan konstruktur/fungsi prototype. Dapat digunakan salah satu dari kedua cara tersebut. Namun, jangan gunakan konstruktur atau prototype klasik secara terpisah, karena hal ini akan memasukkan masalah ke kode.

Contoh

Satu hal yang menarik tentang objek adalah cara mereka memecahkan masalah. Salah satu masalah yang paling sering terjadi di ECMAScript adalah kinerja penggabungan string. Seperti bahasa lainnya, string ECMAScript adalah tak dapat diubah, artinya nilai mereka tidak dapat berubah. Diperhatikan kode di bawah ini:

var str = "hello ";
str += "world";

Faktanya, langkah yang dijalankan secara diam-diam oleh kode ini adalah seperti berikut:

  1. Membuat string untuk menyimpan "hello ";
  2. Membuat string untuk menyimpan "world".
  3. Membuat string untuk menyimpan hasil penghubungan.
  4. Menyalin konten saat ini str ke dalam hasil.
  5. Menyalin "world" ke dalam hasil.
  6. Memperbarui str, supaya menunjuk ke hasil.

Setiap kali operasi penghubungan string selesai, langkah 2 sampai 6 akan dijalankan kembali, sehingga operasi ini sangat memakan sumber daya. Jika proses ini diulang beribu kali, bahkan ribuan kali, hal ini akan menyebabkan masalah kinerja. Solusi adalah menggunakan objek Array untuk menyimpan string, dan kemudian menggunaan metode join() (parameter adalah string kosong) untuk membuat string akhir. Dengan pikirkan kode di bawah ini untuk menggantikan kode sebelumnya:

var arr = new Array();
arr[0] = "hello ";
arr[1] = "world";
var str = arr.join("...");

Dengan cara ini, tak ada masalah apapun jika ada berapa banyak string yang diintrogasi ke dalam array, karena operasi penghubungan hanya berlangsung saat memanggil metode join(). Langkah yang diambil saat ini adalah seperti berikut:

  1. Membuat string untuk menyimpan hasil
  2. Menyalin setiap string ke posisi yang sesuai di dalam hasil

Walaupun solusi ini bagus, tetapi ada cara yang lebih baik. Masalahnya, kode ini tidak dapat menunjukkan dengan jelas kehendaknya. Untuk membuatnya lebih mudah untuk dipahami, dapat memakai kelas StringBuffer untuk mengemas fungsi ini:

function StringBuffer () {
  this._strings_ = new Array();
}
StringBuffer.prototype.append = function(str) {
  this._strings_.push(str);
};
StringBuffer.prototype.toString = function() {
  return this._strings_.join("...");
};

Pada kod ini, yang perlu dicatat adalah properti strings, yang bermakna properti pribadi. Ia hanya memiliki dua metode, yaitu metode append() dan metode toString(). Metode append() memiliki satu parameter, ia menambahkan parameter tersebut ke dalam array string, metode toString() memanggil metode join() array, dan mengembalikan string yang sebenarnya dihubungkan. Untuk menghubungkan beberapa string ke dalam objek StringBuffer, dapat digunakan kode di bawah ini:

var buffer = new StringBuffer ()
buffer.append("hello ");
buffer.append("world");
var result = buffer.toString();

TIY

可用下面的代码测试 StringBuffer 对象和传统的字符串连接方法的性能:

var d1 = new Date();
var str = "";
for (var i=0; i < 10000; i++) {
    str += "text";
}
var d2 = new Date();
document.write("Concatenation with plus: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");
var buffer = new StringBuffer();
d1 = new Date();
for (var i=0; i < 10000; i++) {
    buffer.append("text");
}
var result = buffer.toString();
d2 = new Date();
document.write("<br />Concatenation with StringBuffer: ")
 + (d2.getTime() - d1.getTime()) + " milliseconds");

TIY

这段代码对字符串连接进行两个测试,第一个使用加号,第二个使用 StringBuffer 类。每个操作都连接 10000 个字符串。日期值 d1 和 d2 用于判断完成操作需要的时间。请注意,创建 Date 对象时,如果没有参数,赋予对象的是当前的日期和时间。要计算连接操作历经多少时间,把日期的毫秒表示(用 getTime() 方法的返回值)相减即可。这是衡量 JavaScript 性能的常见方法。该测试的结果可以帮助您比较使用 StringBuffer 类与使用加号的效率差异。