Bab 8: Class

6 menit baca

8.1 Sintaks Dasar Class

💡Analogi

Kalau constructor function itu kayak resep masakan yang ditulis di kertas, maka class itu kayak buku resep yang rapi — isinya sama, tapi lebih terstruktur, lebih mudah dibaca, dan punya aturan tambahan yang mencegah kesalahan.

Sintaks

js
class User {
  constructor(nama) {
    this.nama = nama;
  }

  sapaan() {
    console.log(`Halo, ${this.nama}!`);
  }
}

const user = new User("Budi");
user.sapaan(); // "Halo, Budi!"

Apa Sebenarnya Class Itu?

Class di JavaScript bukan konsep baru — dia tetap function di balik layar:

js
console.log(typeof User); // "function"

// Class = constructor function + prototype methods
console.log(User === User.prototype.constructor); // true
console.log(User.prototype.sapaan); // function sapaan() {...}

Bukan Sekadar Syntactic Sugar

Meski mirip constructor function, class punya perbedaan penting:

  1. Harus pakai new — tidak bisa dipanggil tanpa new
  2. Method non-enumerable — tidak muncul di for..in
  3. Selalu strict mode — kode di dalam class otomatis strict

Class Expression

js
// Seperti function expression
const User = class {
  sapaan() {
    console.log("Halo!");
  }
};

// Named class expression (nama hanya terlihat di dalam class)
const User2 = class MyClass {
  info() {
    console.log(MyClass); // Works di dalam
  }
};
// console.log(MyClass); // Error! Tidak terlihat di luar

Getter/Setter di Class

js
class User {
  constructor(nama) {
    this.nama = nama; // Memanggil setter!
  }

  get nama() {
    return this._nama;
  }

  set nama(value) {
    if (value.length < 3) {
      console.log("Nama terlalu pendek!");
      return;
    }
    this._nama = value;
  }
}

const user = new User("Budi");
console.log(user.nama); // "Budi"

const user2 = new User("Bu"); // "Nama terlalu pendek!"

Class Fields (Properti Langsung)

js
class Button {
  // Class field — disimpan di objek, BUKAN di prototype
  label = "Click me";

  // Arrow function sebagai class field — this selalu terikat!
  click = () => {
    console.log(`Clicked: ${this.label}`);
  }
}

const btn = new Button();
setTimeout(btn.click, 1000); // "Clicked: Click me" — this benar!

Kenapa ini penting? Kalau pakai method biasa dan di-pass ke setTimeout, this akan hilang. Class field dengan arrow function menyelesaikan masalah ini.

⚠️Jebakan!
  1. Tidak ada koma antar method! (Beda dengan object literal)

    js
    class User {
      method1() {} // Tidak ada koma!
      method2() {}
    }
  2. Class field ada di objek, bukan prototype:

    js
    class User {
      nama = "Budi";
    }
    const u = new User();
    console.log(u.nama); // "Budi"
    console.log(User.prototype.nama); // undefined!
🎯Challenge

Buat class Jam yang:

  • Constructor menerima { template } (misal "h:m:s")
  • Punya method render() yang print waktu sekarang sesuai template
  • Punya method start() yang mulai print setiap detik
  • Punya method stop() yang berhenti

8.2 Class Inheritance (Pewarisan)

💡Analogi

Kamu punya class Hewan yang bisa jalan dan berhenti. Sekarang kamu mau bikin Kelinci yang bisa semua yang Hewan bisa, PLUS bisa sembunyi. Daripada copy-paste semua kode, kamu extend (perpanjang) dari Hewan.

Keyword extends

js
class Hewan {
  constructor(nama) {
    this.kecepatan = 0;
    this.nama = nama;
  }

  lari(kecepatan) {
    this.kecepatan = kecepatan;
    console.log(`${this.nama} lari dengan kecepatan ${this.kecepatan}`);
  }

  berhenti() {
    this.kecepatan = 0;
    console.log(`${this.nama} berhenti.`);
  }
}

class Kelinci extends Hewan {
  sembunyi() {
    console.log(`${this.nama} sembunyi!`);
  }
}

const kelinci = new Kelinci("Kelinci Putih");
kelinci.lari(5);     // "Kelinci Putih lari dengan kecepatan 5"
kelinci.sembunyi();  // "Kelinci Putih sembunyi!"

Override Method + super

js
class Kelinci extends Hewan {
  sembunyi() {
    console.log(`${this.nama} sembunyi!`);
  }

  berhenti() {
    super.berhenti(); // Panggil method parent
    this.sembunyi();  // Lalu tambah behavior
  }
}

const k = new Kelinci("Putih");
k.lari(5);
k.berhenti();
// "Putih berhenti."
// "Putih sembunyi!"

Override Constructor — WAJIB super()

js
class Kelinci extends Hewan {
  constructor(nama, panjangTelinga) {
    super(nama); // WAJIB! Panggil constructor parent DULU
    this.panjangTelinga = panjangTelinga;
  }
}

const k = new Kelinci("Putih", 10);
console.log(k.nama);           // "Putih"
console.log(k.panjangTelinga); // 10

Kenapa wajib super()? Di class turunan, objek this belum ada sampai parent constructor membuatnya. Kalau kamu akses this sebelum super(), akan error!

⚠️Jebakan!
  1. Lupa super() di constructor = Error!

    js
    class Anak extends Ortu {
      constructor() {
        // this.x = 1; // ERROR: this belum ada!
        super();       // Harus ini dulu
        this.x = 1;   // Baru boleh pakai this
      }
    }
  2. Arrow function tidak punya super sendiri. Dia ambil dari fungsi luar — ini biasanya yang kita mau:

    js
    class Kelinci extends Hewan {
      berhenti() {
        setTimeout(() => super.berhenti(), 1000); // OK!
      }
    }
  3. Class field override punya perilaku aneh. Parent constructor selalu pakai field miliknya sendiri, bukan yang di-override anak. Solusi: pakai method, bukan field.

🎯Challenge

Buat class Kendaraan dan Mobil:

  • Kendaraan punya constructor (nama, kecepatan) dan method info() yang return "[nama] - [kecepatan] km/h"
  • Mobil extends Kendaraan, tambah parameter merk di constructor
  • Mobil override info() untuk return "[merk] [nama] - [kecepatan] km/h"
  • Pastikan pakai super dengan benar

8.3 Static Properties & Methods

💡Analogi

Method biasa itu milik setiap objek (setiap karyawan bisa kerja()). Static method itu milik class itu sendiri (HRD bisa rekrut() — bukan karyawan individual yang rekrut).

Sintaks

js
class Artikel {
  constructor(judul, tanggal) {
    this.judul = judul;
    this.tanggal = tanggal;
  }

  // Static method — dipanggil di class, bukan di instance
  static bandingkan(a, b) {
    return a.tanggal - b.tanggal;
  }

  // Static property
  static penerbit = "JavaScript.info";
}

const artikels = [
  new Artikel("HTML", new Date(2024, 1, 1)),
  new Artikel("CSS", new Date(2024, 0, 1)),
  new Artikel("JS", new Date(2024, 11, 1))
];

artikels.sort(Artikel.bandingkan);
console.log(artikels[0].judul); // "CSS" (paling lama)

console.log(Artikel.penerbit); // "JavaScript.info"

Factory Method (Pola Umum)

js
class Artikel {
  constructor(judul, tanggal) {
    this.judul = judul;
    this.tanggal = tanggal;
  }

  static buatHariIni() {
    return new this("Digest Hari Ini", new Date());
  }
}

const artikel = Artikel.buatHariIni();
console.log(artikel.judul); // "Digest Hari Ini"

Static Diwariskan!

js
class Hewan {
  static planet = "Bumi";
}

class Kelinci extends Hewan {}

console.log(Kelinci.planet); // "Bumi" — diwariskan!
⚠️Jebakan!

Static method tidak bisa dipanggil dari instance!

js
const a = new Artikel("Test", new Date());
a.bandingkan(...); // ERROR! bandingkan ada di Artikel, bukan di instance
🎯Challenge

Buat class User dengan:

  • Constructor (nama, umur)
  • Static property jumlahUser yang bertambah setiap kali user baru dibuat
  • Static method info() yang return "Total user: [jumlahUser]"

8.4 Private & Protected Properties

💡Analogi

Mesin kopi: dari luar kamu cuma lihat tombol dan display (public). Di dalam ada boiler, pemanas, dll (private). Kamu tidak perlu (dan tidak boleh) utak-atik bagian dalam — cukup tekan tombol.

Protected (Konvensi _)

js
class MesinKopi {
  _jumlahAir = 0; // Protected: jangan akses langsung!

  set jumlahAir(value) {
    if (value < 0) value = 0;
    this._jumlahAir = value;
  }

  get jumlahAir() {
    return this._jumlahAir;
  }
}

const mesin = new MesinKopi();
mesin.jumlahAir = -10;
console.log(mesin.jumlahAir); // 0 (divalidasi!)

_ itu konvensi, bukan aturan bahasa. Programmer sepakat: "jangan akses properti _ dari luar."

Private (Bahasa: #)

js
class MesinKopi {
  #batasAir = 200; // Private: benar-benar tidak bisa diakses dari luar!

  #cekAir(value) {
    if (value < 0) return 0;
    if (value > this.#batasAir) return this.#batasAir;
    return value;
  }

  setAir(value) {
    this.#batasAir = this.#cekAir(value);
  }
}

const mesin = new MesinKopi();
// mesin.#batasAir = 1000; // ERROR! Tidak bisa akses dari luar
// mesin.#cekAir(100);     // ERROR!

Perbedaan Protected vs Private

Protected (_)Private (#)
Enforced oleh bahasa?❌ Konvensi saja✅ Error kalau diakses dari luar
Bisa diakses class turunan?✅ Ya❌ Tidak
Kapan pakai?Kebanyakan kasusKalau benar-benar harus disembunyikan
⚠️Jebakan!
  1. Private field tidak bisa diakses via this[name]:

    js
    this["#field"]; // Tidak works! Ini bukan cara akses private
  2. Private tidak diwariskan. Class anak tidak bisa akses #field milik parent — harus lewat getter/setter public.

🎯Challenge

Buat class BankAccount dengan:

  • Private field #saldo (default 0)
  • Method setor(jumlah) — tambah saldo (tolak kalau jumlah negatif)
  • Method tarik(jumlah) — kurangi saldo (tolak kalau saldo tidak cukup)
  • Getter saldo yang return saldo saat ini
  • Pastikan #saldo tidak bisa diakses langsung dari luar

8.5 Extending Built-in Classes

Konsep

Kamu bisa extend class bawaan seperti Array:

js
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

const arr = new PowerArray(1, 2, 5, 10, 50);
console.log(arr.isEmpty()); // false

const filtered = arr.filter(item => item >= 10);
console.log(filtered); // [10, 50]
console.log(filtered.isEmpty()); // false — masih PowerArray!

Keren: Method bawaan seperti filter, map otomatis return instance dari class turunan (bukan Array biasa)! Ini karena mereka pakai constructor dari objek.

Symbol.species — Kontrol Return Type

js
class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }

  // Paksa filter/map return Array biasa, bukan PowerArray
  static get [Symbol.species]() {
    return Array;
  }
}

const arr = new PowerArray(1, 2, 5);
const filtered = arr.filter(item => item >= 2);
// filtered sekarang Array biasa, bukan PowerArray
// filtered.isEmpty(); // ERROR!
⚠️Jebakan!

Built-in class tidak mewariskan static method! Array.isArray ada, tapi kalau kamu extend Array, MyArray.isArray tetap works karena diwariskan lewat prototype chain class. Tapi Date dan Object tidak saling mewariskan static.

🎯Challenge

Buat class SuperString yang extends String dan punya method isPalindrome() yang cek apakah string itu palindrom.


8.6 Pengecekan Class: instanceof

Cara Pakai

js
class Kelinci {}
const k = new Kelinci();

console.log(k instanceof Kelinci); // true
console.log(k instanceof Object);  // true (semua objek inherit Object)

Works dengan Constructor Function Juga

js
function Kucing() {}
const kucing = new Kucing();
console.log(kucing instanceof Kucing); // true

Works dengan Built-in

js
console.log([1, 2] instanceof Array);  // true
console.log([1, 2] instanceof Object); // true
console.log("hi" instanceof String);   // false! (primitif, bukan objek)

Cara Kerja di Balik Layar

instanceof cek apakah Class.prototype ada di rantai prototype objek:

obj.__proto__ === Class.prototype? obj.__proto__.__proto__ === Class.prototype? // ... dan seterusnya

Object.prototype.toString — "typeof on Steroids"

js
const toString = Object.prototype.toString;

console.log(toString.call(123));       // [object Number]
console.log(toString.call("hi"));      // [object String]
console.log(toString.call(true));      // [object Boolean]
console.log(toString.call(null));      // [object Null]
console.log(toString.call([]));        // [object Array]
console.log(toString.call(function(){})); // [object Function]
⚠️Jebakan!

instanceof cek prototype, bukan constructor! Kalau kamu ganti prototype setelah objek dibuat, instanceof bisa return false:

js
function Kelinci() {}
const k = new Kelinci();

Kelinci.prototype = {}; // Ganti prototype

console.log(k instanceof Kelinci); // false!
🎯Challenge

Buat function cekTipe(value) yang return string deskriptif:

  • Array → "array"
  • null → "null"
  • Object biasa → "object"
  • Function → "function"
  • Number → "number"
  • String → "string"

(Hint: pakai Object.prototype.toString)


8.7 Mixin

💡Analogi

JavaScript hanya boleh inherit dari SATU class. Tapi kadang kamu butuh kemampuan dari banyak sumber. Mixin itu kayak "plugin" — kamu copy method dari objek lain ke prototype class-mu.

Contoh Dasar

js
const sayHiMixin = {
  sayHi() {
    console.log(`Halo, ${this.nama}!`);
  },
  sayBye() {
    console.log(`Dadah, ${this.nama}!`);
  }
};

class User {
  constructor(nama) {
    this.nama = nama;
  }
}

// Copy semua method dari mixin ke prototype User
Object.assign(User.prototype, sayHiMixin);

const user = new User("Budi");
user.sayHi();  // "Halo, Budi!"
user.sayBye(); // "Dadah, Budi!"

Mixin + Inheritance (Bisa Dua-duanya!)

js
class User extends Person {
  // ...
}
Object.assign(User.prototype, sayHiMixin);
// User inherit dari Person DAN punya method dari mixin

EventMixin (Contoh Nyata)

js
const eventMixin = {
  on(eventName, handler) {
    if (!this._handlers) this._handlers = {};
    if (!this._handlers[eventName]) this._handlers[eventName] = [];
    this._handlers[eventName].push(handler);
  },

  off(eventName, handler) {
    const handlers = this._handlers?.[eventName];
    if (!handlers) return;
    const index = handlers.indexOf(handler);
    if (index !== -1) handlers.splice(index, 1);
  },

  trigger(eventName, ...args) {
    if (!this._handlers?.[eventName]) return;
    this._handlers[eventName].forEach(h => h.apply(this, args));
  }
};

// Pakai di class mana saja:
class Menu {
  pilih(value) {
    this.trigger("select", value);
  }
}
Object.assign(Menu.prototype, eventMixin);

const menu = new Menu();
menu.on("select", value => console.log(`Dipilih: ${value}`));
menu.pilih("Nasi Goreng"); // "Dipilih: Nasi Goreng"
⚠️Jebakan!
  1. Nama method bisa bentrok! Kalau dua mixin punya method dengan nama sama, yang terakhir di-assign menang.
  2. Mixin bukan inheritance. instanceof tidak akan mendeteksi mixin.
🎯Challenge

Buat loggerMixin yang punya:

  • log(pesan) — print "[NamaClass] pesan"
  • warn(pesan) — print "⚠️ [NamaClass] pesan"
  • error(pesan) — print "❌ [NamaClass] pesan"

Lalu terapkan ke class Server dan Database. Pastikan nama class-nya benar di output.

(Hint: this.constructor.name untuk dapat nama class)

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.