kodingup.camp

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

Konsep Pewarisan Lanjutan

Modul 4 dari 153 menit bacaAdvanced Inheritance Concepts

Isi modul

    Kenapa ini penting

    Setelah paham prototype chain, pertanyaan berikutnya: bagaimana membangun hierarki yang rapi? Misalnya di aplikasi toko online kamu punya Pengguna, lalu Pembeli dan Penjual yang mewarisi kemampuan dasar Pengguna tetapi punya perilaku tambahan masing-masing. Inilah pewarisan (inheritance).

    Namun pewarisan bukan satu-satunya alat, dan sering kali bukan yang terbaik. Programmer berpengalaman punya prinsip terkenal: "favor composition over inheritance", utamakan komposisi daripada pewarisan. Di modul ini kamu belajar tiga teknik: pewarisan via prototype (cara kerja asli di balik class extends), method override (anak mengganti perilaku orang tua), dan mixin serta komposisi (merakit kemampuan seperti memasang modul LEGO). Dengan begitu kamu bisa memilih alat yang tepat, bukan sekadar memaksa semuanya menjadi pohon keluarga.

    Konsep inti

    Pewarisan via prototype (cara manual)

    Agar Pembeli mewarisi Pengguna, ada dua langkah:

    1. Warisi properti instance: panggil konstruktor induk dengan this milik anak, Pengguna.call(this, nama).
    2. Warisi method: hubungkan prototype, Pembeli.prototype = Object.create(Pengguna.prototype), lalu kembalikan constructor yang benar.

    Rantainya menjadi: instancePembeli → Pembeli.prototype → Pengguna.prototype → Object.prototype → null.

    Kamu jarang menulis ini secara manual sekarang karena ada class extends (modul berikutnya). Tetapi sintaks class hanyalah "kemasan rapi" untuk mekanisme yang persis sama. Memahaminya membuat kamu tidak bingung saat membaca kode lama atau error yang menyebut prototype.

    Method override

    Jika anak punya method dengan nama yang sama dengan induk, method anak yang dipakai karena ditemukan lebih dulu di rantai. Kalau masih ingin memakai versi induk, panggil eksplisit: Pengguna.prototype.info.call(this) (di class cukup super.info()).

    Mixin

    Mixin adalah kumpulan method yang "ditempelkan" ke object atau prototype dengan Object.assign. Berguna saat beberapa kelas yang tidak berkerabat butuh kemampuan yang sama, misalnya kemampuan "bisa disimpan ke JSON" atau "bisa memancarkan event".

    Object.assign(Produk.prototype, bisaDiserialisasi);
    

    Komposisi vs pewarisan

    Pewarisan ("adalah") Komposisi ("punya" / "bisa")
    Pembeli adalah Pengguna Robot punya kemampuan jalan dan bicara
    Hierarki tetap, ditentukan di awal Kemampuan dirakit fleksibel
    Perubahan di induk merembet ke semua anak Kemampuan independen, mudah diganti
    Risiko hierarki terlalu dalam ("gorilla-banana problem") Bisa jadi banyak potongan kecil

    Masalah klasik pewarisan: kamu membuat Burung dengan method terbang(), lalu muncul Penguin. Penguin adalah burung tetapi tidak bisa terbang. Dengan komposisi, kamu cukup merakit Penguin dari kemampuan berenang dan berjalan tanpa terbang.

    Aturan praktis: pakai pewarisan jika hubungannya benar-benar "X adalah jenis khusus dari Y" dan hierarkinya dangkal (1-2 tingkat). Untuk berbagi kemampuan lintas jenis, pakai komposisi atau mixin.

    Contoh kode

    // ===== 1. Pewarisan via prototype (manual) =====
    function Pengguna(nama, email) {
      this.nama = nama;
      this.email = email;
    }
    Pengguna.prototype.info = function () {
      return `${this.nama} <${this.email}>`;
    };
    Pengguna.prototype.peran = function () {
      return "pengguna";
    };
    
    function Penjual(nama, email, namaToko) {
      Pengguna.call(this, nama, email); // warisi properti instance
      this.namaToko = namaToko;
    }
    // warisi method
    Penjual.prototype = Object.create(Pengguna.prototype);
    Penjual.prototype.constructor = Penjual;
    
    // override method peran
    Penjual.prototype.peran = function () {
      return "penjual";
    };
    // override sambil memanggil versi induk
    Penjual.prototype.info = function () {
      const dasar = Pengguna.prototype.info.call(this);
      return `${dasar} | Toko: ${this.namaToko}`;
    };
    
    const s = new Penjual("Rudi", "rudi@toko.id", "Rudi Elektronik");
    console.log(s.info());  // → Rudi <rudi@toko.id> | Toko: Rudi Elektronik
    console.log(s.peran()); // → penjual
    console.log(s instanceof Penjual, s instanceof Pengguna); // → true true
    
    // ===== 2. Mixin =====
    const bisaDiserialisasi = {
      keJSON() { return JSON.stringify(this); },
    };
    const punyaTimestamp = {
      tandaiDibuat() { this.dibuatPada = "2026-10-23"; return this; },
    };
    Object.assign(Penjual.prototype, bisaDiserialisasi, punyaTimestamp);
    
    console.log(s.tandaiDibuat().keJSON());
    // → {"nama":"Rudi","email":"rudi@toko.id","namaToko":"Rudi Elektronik","dibuatPada":"2026-10-23"}
    
    // ===== 3. Komposisi: merakit kemampuan =====
    const bisaJalan = (state) => ({
      jalan: () => `${state.nama} berjalan dengan ${state.kaki} kaki`,
    });
    const bisaTerbang = (state) => ({
      terbang: () => `${state.nama} terbang di ketinggian ${state.ketinggian} m`,
    });
    const bisaBerenang = (state) => ({
      berenang: () => `${state.nama} berenang`,
    });
    
    function buatElang(nama) {
      const state = { nama, kaki: 2, ketinggian: 300 };
      return { ...bisaJalan(state), ...bisaTerbang(state) };
    }
    function buatPenguin(nama) {
      const state = { nama, kaki: 2 };
      return { ...bisaJalan(state), ...bisaBerenang(state) }; // tanpa terbang
    }
    
    const elang = buatElang("Garuda");
    const penguin = buatPenguin("Pingu");
    console.log(elang.terbang());   // → Garuda terbang di ketinggian 300 m
    console.log(penguin.berenang()); // → Pingu berenang
    console.log(typeof penguin.terbang); // → undefined (memang tidak punya)
    
    // ===== 4. Komposisi object di dalam object (has-a) =====
    function buatMesin(tenaga) {
      return { nyalakan: () => `Mesin ${tenaga} HP menyala` };
    }
    function buatMobil(merk) {
      const mesin = buatMesin(150); // mobil PUNYA mesin
      return {
        merk,
        mulai() { return `${merk}: ${mesin.nyalakan()}`; },
      };
    }
    console.log(buatMobil("Avanza").mulai()); // → Avanza: Mesin 150 HP menyala
    

    Perhatikan pada bagian komposisi, state disimpan di closure. Tidak ada this, tidak ada prototype, dan setiap kemampuan bisa diuji sendiri-sendiri. Konsep closure yang sudah kamu pelajari menjadi sangat berguna di sini.

    Kesalahan umum

    1. Lupa Pengguna.call(this, ...). Method induk terwarisi, tetapi properti seperti nama tidak pernah diisi sehingga bernilai undefined.
    2. Menulis Penjual.prototype = Pengguna.prototype. Ini bukan pewarisan, tapi berbagi object yang sama; menambah method di Penjual ikut mengubah Pengguna. Pakai Object.create.
    3. Lupa mengembalikan constructor. Setelah Object.create, Penjual.prototype.constructor menunjuk ke Pengguna. Set ulang agar tidak membingungkan.
    4. Hierarki terlalu dalam. Lima tingkat pewarisan membuat perubahan kecil di atas merusak banyak hal di bawah. Pertimbangkan komposisi.
    5. Mixin yang bentrok nama. Dua mixin dengan method bernama sama saling menimpa tanpa peringatan (yang terakhir menang). Beri nama yang spesifik.

    Latihan

    1. Mudah, Buat konstruktor Bentuk(nama) dengan method deskripsi(). Buat Persegi(sisi) yang mewarisi Bentuk dan menambah method luas().
    2. Sedang, Override deskripsi() di Persegi agar memanggil versi induk lalu menambahkan " dengan luas X".
    3. Menantang, Dengan komposisi, buat buatKaryawan(nama) dan buatFreelancer(nama). Keduanya bisa hitungGaji, tetapi hanya karyawan yang ambilCuti. Buat kemampuan sebagai function terpisah.
    Lihat solusi
    // 1 & 2
    function Bentuk(nama) { this.nama = nama; }
    Bentuk.prototype.deskripsi = function () { return `Ini adalah ${this.nama}`; };
    
    function Persegi(sisi) {
      Bentuk.call(this, "persegi");
      this.sisi = sisi;
    }
    Persegi.prototype = Object.create(Bentuk.prototype);
    Persegi.prototype.constructor = Persegi;
    Persegi.prototype.luas = function () { return this.sisi ** 2; };
    Persegi.prototype.deskripsi = function () {
      return `${Bentuk.prototype.deskripsi.call(this)} dengan luas ${this.luas()}`;
    };
    console.log(new Persegi(4).deskripsi()); // → Ini adalah persegi dengan luas 16
    
    // 3
    const bisaDigaji = (s) => ({ hitungGaji: () => s.tarif * s.jam });
    const bisaCuti = (s) => ({
      ambilCuti(hari) { s.sisaCuti -= hari; return `Sisa cuti ${s.nama}: ${s.sisaCuti}`; },
    });
    function buatKaryawan(nama) {
      const s = { nama, tarif: 50000, jam: 160, sisaCuti: 12 };
      return { nama, ...bisaDigaji(s), ...bisaCuti(s) };
    }
    function buatFreelancer(nama) {
      const s = { nama, tarif: 120000, jam: 40 };
      return { nama, ...bisaDigaji(s) };
    }
    const k = buatKaryawan("Tono"), f = buatFreelancer("Lia");
    console.log(k.hitungGaji(), k.ambilCuti(2)); // → 8000000 Sisa cuti Tono: 10
    console.log(f.hitungGaji(), typeof f.ambilCuti); // → 4800000 undefined
    

    Ringkasan

    Kuis pemahaman

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