Ketika data dan perilaku selalu berjalan bersama — tugas yang punya judul, durasi, dan cara menandai selesai — membentuknya sebagai object lepas menjadi rapuh: setiap object harus membawa method-nya sendiri, dan tidak ada jaminan bentuk yang seragam. Class menjawab kebutuhan itu dengan cetak biru (blueprint): satu definisi yang bisa dipakai mencetak banyak instance seragam. Di Node.js, class kita temui di http.Server, stream, hingga class Error sendiri. Pada chapter ini kita akan menulis class dengan constructor dan method, melindungi data lewat getter dan setter, membangun hierarki dengan extends dan super, lalu menyelami prototype chain yang bekerja diam-diam di balik sintaks class.

Class, constructor, dan Method #

Kata kunci class mendefinisikan cetak biru; operator new mencetak instance baru darinya. Method khusus bernama constructor dijalankan sekali untuk setiap instance baru dan menjadi tempat menyiapkan data awal lewat this. Simpan berikut sebagai kelas-dasar.mjs lalu jalankan dengan node kelas-dasar.mjs:

js
class Tugas {
  constructor(judul, durasiJam) {
    this.judul = judul;
    this.durasiJam = durasiJam;
    this.selesai = false;
  }

  tandaiSelesai() {
    this.selesai = true;
  }

  ringkas() {
    const status = this.selesai ? "selesai" : "berjalan";
    return `${this.judul} (${status}, ${this.durasiJam} jam)`;
  }
}

const belajar = new Tugas("Belajar Node.js", 2);
const olahraga = new Tugas("Lari pagi", 1);

belajar.tandaiSelesai();

console.log(belajar.ringkas());
console.log(olahraga.ringkas());
console.log(`belajar instanceof Tugas : ${belajar instanceof Tugas}`);

Output yang Dihasilkan:

text
Belajar Node.js (selesai, 2 jam)
Lari pagi (berjalan, 1 jam)
belajar instanceof Tugas : true

Mari kita telaah baris-baris kode di atas:

  1. Baris 2-7 (constructor): setiap new Tugas(...) menjalankan constructor ini. this menunjuk instance yang baru diciptakan; tiga property dipasang dengan nilai dari argumen, dan selesai selalu dimulai dari false.
  2. Baris 9-11 (tandaiSelesai): method mengubah state instance. Method didefinisikan sekali di class, tetapi dipanggil melalui instance — belajar.tandaiSelesai() hanya mengubah instance belajar, bukan olahraga.
  3. Baris 13-17 (ringkas): method membaca state terkini dan merangkainya menjadi string. Perhatikan bahwa olahraga tetap "berjalan" karena hanya belajar yang ditandai selesai.
  4. Baris terakhir (instanceof): operator ini menguji apakah sebuah instance diciptakan dari class tertentu — berguna untuk validasi sebelum memanggil method-nya.

Perlu dicatat bahwa dalam class, this pada method menunjuk instance pemanggil — aturan dynamic binding yang sama dengan method object biasa pada Chapter Function dan Arrow Function. Konsekuensinya juga sama: arrow function tidak cocok dipakai sebagai method class karena this-nya tidak menunjuk instance.

Getter dan Setter #

Getter dan setter membuat method terlihat seperti property biasa dari luar class. Manfaat utamanya adalah enkapsulasi: kode luar tetap menulis artikel.jumlahKata = 450 seperti memberi nilai property, sementara class menjalankan validasi di pintu masuk dan menyimpan nilai aslinya di property internal _jumlahKata. Berkas getter-setter.mjs memperlihatkan pola lengkapnya:

js
class Artikel {
  constructor(judul) {
    this.judul = judul;
    this._jumlahKata = 0;
  }

  // getter: dibaca seperti property biasa
  get jumlahKata() {
    return this._jumlahKata;
  }

  // setter: jalankan validasi sebelum nilai disimpan
  set jumlahKata(nilai) {
    if (!Number.isInteger(nilai) || nilai < 0) {
      throw new Error(`jumlah kata tidak valid: ${nilai}`);
    }
    this._jumlahKata = nilai;
  }

  // property terhitung dari data lain
  get durasiBacaMenit() {
    return Math.ceil(this._jumlahKata / 200);
  }
}

const artikel = new Artikel("Panduan Node.js");
artikel.jumlahKata = 450;

console.log(`judul       : ${artikel.judul}`);
console.log(`jumlah kata : ${artikel.jumlahKata}`);
console.log(`durasi baca : ${artikel.durasiBacaMenit} menit`);

try {
  artikel.jumlahKata = -10;
} catch (e) {
  console.log(`${e.name}: ${e.message}`);
}

Output yang Dihasilkan:

text
judul       : Panduan Node.js
jumlah kata : 450
durasi baca : 3 menit
Error: jumlah kata tidak valid: -10

Alur eksekusinya:

  1. Baris 25 (artikel.jumlahKata = 450): penugasan ini tidak menulis property secara langsung, melainkan memanggil setter. Validasi lolos karena 450 adalah bilangan bulat positif, lalu nilai disimpan ke _jumlahKata.
  2. Baris 27 (artikel.jumlahKata): pembacaan tanpa tanda sama dengan memanggil getter, yang mengembalikan nilai dari _jumlahKata.
  3. durasiBacaMenit: getter murni tanpa pasangan setter — property hitungan Math.ceil(450 / 200) yang selalu segar setiap kali dibaca, dengan asumsi kecepatan baca 200 kata per menit.
  4. Baris 31-34: penugasan -10 ditolak setter dengan melempar Error. Blok try...catch menangkapnya sehingga program tetap berjalan — teknik penanganan error yang akan kita bahas tuntas pada chapter berikutnya.

Inheritance dengan extends dan super #

Kata kunci extends membangun class anak yang mewarisi seluruh method class induk, dan super menjadi jembatan ke induk itu: super(...) memanggil constructor induk, super.method() memanggil method-nya. Method anak dengan nama yang sama menimpa (override) versi induk. Ada pula member static yang melekat pada class itu sendiri, bukan pada instance — tempat yang tepat untuk factory seperti buatRoot(). Semuanya terangkum dalam warisan-statis.mjs:

js
class Pengguna {
  constructor(nama, email) {
    this.nama = nama;
    this.email = email;
  }

  perkenalan() {
    return `${this.nama} (${this.email})`;
  }
}

class Admin extends Pengguna {
  constructor(nama, email, level) {
    super(nama, email); // wajib sebelum memakai this
    this.level = level;
  }

  perkenalan() {
    return `[${this.level}] ${super.perkenalan()}`;
  }

  // method statis: melekat pada class, bukan pada instance
  static buatRoot(nama) {
    return new Admin(nama, `${nama.toLowerCase()}@example.com`, "root");
  }
}

const admin = new Admin("Budi", "[email protected]", "staf");
const root = Admin.buatRoot("Siti");

console.log(admin.perkenalan());
console.log(root.perkenalan());
console.log(`admin instanceof Pengguna : ${admin instanceof Pengguna}`);

Output yang Dihasilkan:

text
[staf] Budi ([email protected])
[root] Siti ([email protected])
admin instanceof Pengguna : true

Beberapa hal penting dari kode di atas:

  1. super(nama, email) di constructor Admin: class anak yang punya constructor sendiri wajib memanggil super() sebelum menyentuh this — kalau tidak, eksekusi melempar ReferenceError.
  2. perkenalan() versi Admin menimpa versi Pengguna, tetapi tetap bisa memanggil versi induk lewat super.perkenalan() lalu membungkusnya dengan label level.
  3. Admin.buatRoot("Siti"): method statis dipanggil langsung dari class tanpa new. Factory seperti ini menyembunyikan detail pembuatan (email dari nama, level "root") dari pemanggil.
  4. admin instanceof Pengguna bernilai true: instance anak juga dianggap instance induk — dasar dari pola substitusi dalam object-oriented programming.

Prototype di Balik Class #

Sintaks class adalah lapisan manis di atas mekanisme yang lebih tua: prototype chain. Setiap class menyimpan method-methodnya di sebuah object bernama Pengguna.prototype (bisa diakses sebagai Class.prototype), dan setiap instance menyimpan tautan internal — dikenal sebagai __proto__ secara konseptual — ke object prototype milik class-nya. Saat kita menulis kucing.bersuara(), JavaScript mencari property bersuara pada instance lebih dulu; bila tidak ada, pencarian menyusuri rantai prototype. Perhatikan buktinya:

js
class Hewan {
  bersuara() {
    return "...";
  }
}

const kucing = new Hewan();

console.log(`milik instance      : ${Object.hasOwn(kucing, "bersuara")}`);
console.log(`tipe di prototype   : ${typeof Hewan.prototype.bersuara}`);
console.log(`prototype kucing    : ${Object.getPrototypeOf(kucing) === Hewan.prototype}`);
console.log(`induk prototype     : ${Object.getPrototypeOf(Hewan.prototype) === Object.prototype}`);

Output yang Dihasilkan:

text
milik instance      : false
tipe di prototype   : function
prototype kucing    : true
induk prototype     : true

Baris pertama membuktikan method tidak disalin ke setiap instance — bersuara bukan milik kucing sendiri. Baris kedua menunjukkan tempat sebenarnya: Hewan.prototype.bersuara adalah function. Dua baris terakhir memetakan rantainya: prototype kucing adalah Hewan.prototype, dan prototype dari Hewan.prototype adalah Object.prototype — ujung rantai tempat method seperti toString() tinggal. Inilah sebabnya instance Tugas kita bisa memanggil belajar.toString() meskipun kita tidak pernah menulis method itu.

Referensi lanjutan untuk topik chapter ini:

Class kini lengkap di tangan kita: constructor, getter/setter, warisan, sampai prototype yang menopangnya semua. Dalam contoh-contoh di atas, beberapa method dengan sengaja melempar Error ketika menerima masukan tidak valid — dan kita selalu menangkapnya dengan try...catch. Bab berikutnya membedah mekanisme itu secara menyeluruh: bentuk object Error, alur try-catch-finally, hingga cara membuat class error kustom. Lanjut ke Chapter Penanganan Error dengan try-catch.