Kenapa ini penting
Di dua modul sebelumnya kamu membangun pewarisan dengan tangan: fungsi konstruktor, Object.create, menyetel ulang constructor. Hasilnya benar tapi bertele-tele dan rawan lupa satu langkah. Sejak ES2015, JavaScript punya sintaks class yang membungkus semua itu dengan rapi. Seiring waktu, class mendapat fitur baru yang tidak bisa ditiru dengan mudah oleh cara lama, seperti private field # yang benar-benar tersembunyi.
class ada di mana-mana: komponen di beberapa framework, model data, service di backend Node.js, sampai objek bawaan seperti Error, Map, dan Promise. Analoginya, class adalah cetakan kue. Cetakannya satu, kuenya (instance) bisa banyak, masing-masing dengan isi berbeda tetapi bentuk dan kemampuan yang sama. Yang terpenting: kamu sekarang tahu bahwa di balik class tetap ada prototype chain. class bukan sistem baru, hanya cara menulis yang lebih jelas.
Konsep inti
Anatomi class
class NamaClass {
fieldPublik = 0; // field instance (per object)
#fieldPrivat = "rahasia"; // hanya bisa diakses di dalam class
static jumlah = 0; // milik class, bukan instance
constructor(param) { /* dijalankan saat new */ }
method() { /* masuk ke NamaClass.prototype */ }
get nilai() { /* getter */ }
set nilai(v) { /* setter */ }
static buat() { /* dipanggil lewat NamaClass.buat() */ }
#methodPrivat() { /* method privat */ }
}
| Bagian | Tempat tinggal | Diakses lewat |
|---|---|---|
constructor |
Function class itu sendiri | new NamaClass() |
| Method biasa | NamaClass.prototype |
instance.method() |
| Field instance | Setiap object | instance.field |
static |
Class | NamaClass.x |
#privat |
Setiap object, terkunci | Hanya di dalam body class |
extends dan super
class Anak extends Indukmenghubungkan prototype secara otomatis.- Di constructor anak, wajib memanggil
super(...)sebelum memakaithis. super.method()memanggil versi induk dari method yang di-override.
Private field #
Berbeda dari konvensi lama _nama (hanya kesepakatan, tetap bisa diakses), #nama benar-benar tidak bisa diakses dari luar. Mencoba obj.#nama di luar class adalah SyntaxError. Untuk mengecek apakah object punya private field tertentu, pakai #nama in obj di dalam class.
instanceof
obj instanceof Kelas mengecek apakah Kelas.prototype ada di rantai prototype obj. Jadi instance anak juga instanceof induk.
Hal yang perlu diingat
- Body class selalu berjalan dalam strict mode.
- Class tidak di-hoist seperti function: memakai class sebelum deklarasinya memunculkan
ReferenceError. - Memanggil class tanpa
newmemunculkanTypeError. typeof NamaClassadalah"function", bukti bahwa class tetap function di baliknya.
Contoh kode
class Rekening {
static #jumlahRekening = 0; // private static
static BUNGA_TAHUNAN = 0.03;
#saldo = 0; // private field
#riwayat = [];
constructor(pemilik, saldoAwal = 0) {
if (!pemilik) throw new Error("Pemilik wajib diisi");
this.pemilik = pemilik;
this.nomor = `REK-${String(++Rekening.#jumlahRekening).padStart(4, "0")}`;
if (saldoAwal > 0) this.setor(saldoAwal);
}
get saldo() {
return this.#saldo;
}
setor(jumlah) {
this.#validasi(jumlah);
this.#saldo += jumlah;
this.#riwayat.push({ jenis: "setor", jumlah });
return this; // supaya bisa chaining
}
tarik(jumlah) {
this.#validasi(jumlah);
if (jumlah > this.#saldo) throw new Error("Saldo tidak cukup");
this.#saldo -= jumlah;
this.#riwayat.push({ jenis: "tarik", jumlah });
return this;
}
get riwayat() {
return [...this.#riwayat]; // salinan, supaya data asli aman
}
#validasi(jumlah) {
if (!Number.isFinite(jumlah) || jumlah <= 0) {
throw new RangeError("Jumlah harus angka positif");
}
}
static jumlah() {
return Rekening.#jumlahRekening;
}
toString() {
return `${this.nomor} (${this.pemilik}): Rp${this.#saldo.toLocaleString("id-ID")}`;
}
}
const r1 = new Rekening("Sari", 500000);
r1.setor(250000).tarik(100000);
console.log(String(r1)); // → REK-0001 (Sari): Rp650.000
console.log(r1.saldo); // → 650000
console.log(r1.riwayat.length); // → 3
r1.saldo = 999999999; // hanya ada getter → penulisan diabaikan (file non-strict)
console.log(r1.saldo); // → 650000
console.log(Object.keys(r1)); // → [ 'pemilik', 'nomor' ] (private tidak terlihat)
// ===== Pewarisan dengan extends & super =====
class RekeningTabungan extends Rekening {
#batasTarikPerHari;
constructor(pemilik, saldoAwal, batas = 2000000) {
super(pemilik, saldoAwal); // wajib sebelum memakai this
this.#batasTarikPerHari = batas;
}
tarik(jumlah) { // override
if (jumlah > this.#batasTarikPerHari) {
throw new Error(`Melebihi batas harian Rp${this.#batasTarikPerHari.toLocaleString("id-ID")}`);
}
return super.tarik(jumlah); // panggil versi induk
}
hitungBunga() {
return Math.round(this.saldo * Rekening.BUNGA_TAHUNAN);
}
toString() {
return `[Tabungan] ${super.toString()}`;
}
}
const t = new RekeningTabungan("Budi", 10000000);
try {
t.tarik(5000000);
} catch (e) {
console.log(e.message); // → Melebihi batas harian Rp2.000.000
}
t.tarik(1500000);
console.log(String(t)); // → [Tabungan] REK-0002 (Budi): Rp8.500.000
console.log(t.hitungBunga()); // → 255000
console.log(Rekening.jumlah()); // → 2
// ===== instanceof & bukti prototype =====
console.log(t instanceof RekeningTabungan, t instanceof Rekening); // → true true
console.log(r1 instanceof RekeningTabungan); // → false
console.log(typeof Rekening); // → function
console.log(Object.getPrototypeOf(RekeningTabungan.prototype) === Rekening.prototype); // → true
// ===== Static factory method =====
class Suhu {
constructor(celsius) { this.celsius = celsius; }
static dariFahrenheit(f) { return new Suhu((f - 32) * 5 / 9); }
get fahrenheit() { return this.celsius * 9 / 5 + 32; }
}
console.log(Suhu.dariFahrenheit(98.6).celsius.toFixed(1)); // → 37.0
Ada satu baris menarik di atas: r1.saldo = 999999999. Karena saldo hanya punya getter, penulisan diabaikan di kode non-strict (di file biasa ini). Kalau baris itu berada di dalam class atau ES module (strict), akan muncul TypeError. Dalam kedua kasus, saldo tetap aman. Inilah gunanya enkapsulasi: data penting hanya bisa diubah lewat pintu resmi (setor, tarik) yang punya validasi.
Kesalahan umum
- Memakai
thissebelumsuper(). Di constructor class turunan, ini memunculkanReferenceError: Must call super constructor.... Panggilsuper()di baris pertama. - Kehilangan
thissaat method dilepas.const f = r1.setor; f(10)membuatthisbernilaiundefined. Gunakanr1.setor.bind(r1)atau arrow function pembungkus, misalnya saat memakai method sebagai event handler. - Mengira
_namaitu privat. Garis bawah hanya konvensi. Gunakan#namauntuk privasi sungguhan. - Memanggil static method dari instance.
r1.jumlah()error karenajumlahmilik class. PanggilRekening.jumlah(). - Membuat getter dan field dengan nama sama, misalnya field
saldodanget saldo(). Field instance akan menutupi getter di prototype. Pakai private field#saldountuk penyimpanan.
Latihan
- Mudah, Buat class
Persegidengan constructor(sisi), getterluas, dan methodkeliling(). - Sedang, Buat class
Produkdengan private field#stok, methodtambahStok(n)danjual(n)(lempar error jika stok kurang), serta getterstok. Tambahkanstatic dariObjek({ nama, harga, stok }). - Menantang, Buat class
ProdukDigital extends Produkyang stoknya tidak terbatas: overridejual()agar tidak mengurangi stok, dantoString()yang memakaisuper.toString().
Lihat solusi
// 1
class Persegi {
constructor(sisi) { this.sisi = sisi; }
get luas() { return this.sisi ** 2; }
keliling() { return 4 * this.sisi; }
}
const p = new Persegi(5);
console.log(p.luas, p.keliling()); // → 25 20
// 2
class Produk {
#stok;
constructor(nama, harga, stok = 0) {
this.nama = nama;
this.harga = harga;
this.#stok = stok;
}
get stok() { return this.#stok; }
tambahStok(n) { this.#stok += n; return this; }
jual(n) {
if (n > this.#stok) throw new Error(`Stok ${this.nama} tidak cukup`);
this.#stok -= n;
return this.harga * n;
}
toString() { return `${this.nama} @Rp${this.harga}`; }
static dariObjek({ nama, harga, stok }) { return new Produk(nama, harga, stok); }
}
const kopi = Produk.dariObjek({ nama: "Kopi", harga: 25000, stok: 3 });
console.log(kopi.jual(2), kopi.stok); // → 50000 1
try { kopi.jual(5); } catch (e) { console.log(e.message); } // → Stok Kopi tidak cukup
// 3
class ProdukDigital extends Produk {
jual(n) { return this.harga * n; } // tanpa mengurangi stok
toString() { return `${super.toString()} (digital)`; }
}
const ebook = new ProdukDigital("E-book JS", 99000);
console.log(ebook.jual(100), String(ebook)); // → 9900000 E-book JS @Rp99000 (digital)
console.log(ebook instanceof Produk); // → true
Ringkasan
classadalah sintaks rapi di atas prototype; method masuk keKelas.prototype.#fielddan#method()benar-benar privat;_namahanya konvensi.staticmilik class, dipakai untuk konstanta, penghitung, dan factory method.extends+super()untuk pewarisan;super.method()memanggil versi induk.instanceofmengecek keberadaanKelas.prototypedi rantai prototype object.