kodingup.camp

Beranda/JavaScript Mastery: Prototype, Class, Waktu, Error & Async

Arsitektur Berbasis Class

Modul 5 dari 153 menit bacaClass-Based Architecture

Isi modul

    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

    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

    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

    1. Memakai this sebelum super(). Di constructor class turunan, ini memunculkan ReferenceError: Must call super constructor.... Panggil super() di baris pertama.
    2. Kehilangan this saat method dilepas. const f = r1.setor; f(10) membuat this bernilai undefined. Gunakan r1.setor.bind(r1) atau arrow function pembungkus, misalnya saat memakai method sebagai event handler.
    3. Mengira _nama itu privat. Garis bawah hanya konvensi. Gunakan #nama untuk privasi sungguhan.
    4. Memanggil static method dari instance. r1.jumlah() error karena jumlah milik class. Panggil Rekening.jumlah().
    5. Membuat getter dan field dengan nama sama, misalnya field saldo dan get saldo(). Field instance akan menutupi getter di prototype. Pakai private field #saldo untuk penyimpanan.

    Latihan

    1. Mudah, Buat class Persegi dengan constructor (sisi), getter luas, dan method keliling().
    2. Sedang, Buat class Produk dengan private field #stok, method tambahStok(n) dan jual(n) (lempar error jika stok kurang), serta getter stok. Tambahkan static dariObjek({ nama, harga, stok }).
    3. Menantang, Buat class ProdukDigital extends Produk yang stoknya tidak terbatas: override jual() agar tidak mengurangi stok, dan toString() yang memakai super.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

    Kuis pemahaman

    Pilih satu jawaban per soal. Penjelasan muncul setelah kamu menjawab.