Bab 7: Prototype & Inheritance

4 menit baca

7.1 Prototypal Inheritance

💡Analogi

Bayangin kamu punya buku resep keluarga. Kalau kamu mau masak dan resepnya tidak ada di buku pribadimu, kamu buka buku resep orang tua. Kalau di situ juga tidak ada, buka buku resep nenek. Itulah prototype chain — rantai pencarian "warisan" dari satu objek ke objek lain.

Konsep [[Prototype]]

Setiap objek di JavaScript punya properti tersembunyi [[Prototype]] yang menunjuk ke objek lain (atau null). Saat kamu akses properti yang tidak ada di objek, JavaScript otomatis cari di prototype-nya.

js
const hewan = {
  makan: true,
  jalan() {
    console.log("Jalan-jalan...");
  }
};

const kelinci = {
  lompat: true,
  __proto__: hewan // Set prototype kelinci = hewan
};

console.log(kelinci.makan);  // true (dari hewan)
console.log(kelinci.lompat); // true (milik sendiri)
kelinci.jalan(); // "Jalan-jalan..." (dari hewan)

Rantai Prototype Bisa Panjang

js
const hewan = {
  makan: true,
  jalan() { console.log("Jalan..."); }
};

const kelinci = {
  lompat: true,
  __proto__: hewan
};

const kelinciKecil = {
  panjangTelinga: 5,
  __proto__: kelinci
};

kelinciKecil.jalan(); // "Jalan..." (dari hewan, lewat kelinci)
console.log(kelinciKecil.lompat); // true (dari kelinci)

Rantai: kelinciKecilkelincihewanObject.prototypenull

Prototype hanya dipakai untuk membaca. Kalau kamu menulis properti, langsung disimpan di objek itu sendiri:

js
const hewan = {
  jalan() {
    console.log("Hewan jalan");
  }
};

const kelinci = {
  __proto__: hewan
};

// Menulis method baru langsung di kelinci
kelinci.jalan = function() {
  console.log("Kelinci lompat-lompat!");
};

kelinci.jalan(); // "Kelinci lompat-lompat!" (milik sendiri)

Nilai this Selalu Objek Sebelum Titik

Ini penting banget! Mau method-nya dari prototype mana pun, this selalu menunjuk ke objek yang memanggil:

js
const hewan = {
  tidur() {
    this.sedangTidur = true;
  }
};

const kelinci = {
  nama: "Kelinci Putih",
  __proto__: hewan
};

kelinci.tidur(); // this = kelinci

console.log(kelinci.sedangTidur); // true
console.log(hewan.sedangTidur);   // undefined (hewan tidak terpengaruh!)

for..in Iterasi Properti Warisan Juga

js
const hewan = { makan: true };
const kelinci = { lompat: true, __proto__: hewan };

// Object.keys hanya milik sendiri
console.log(Object.keys(kelinci)); // ["lompat"]

// for..in termasuk warisan
for (let prop in kelinci) {
  console.log(prop); // "lompat", lalu "makan"
}

// Filter hanya milik sendiri:
for (let prop in kelinci) {
  if (kelinci.hasOwnProperty(prop)) {
    console.log(`Milik sendiri: ${prop}`); // "lompat"
  }
}
⚠️Jebakan!
  1. __proto__ itu getter/setter, bukan properti biasa. Dia cara lama untuk akses [[Prototype]]. Cara modern: Object.getPrototypeOf() / Object.setPrototypeOf().
  2. Prototype chain tidak boleh melingkar. JavaScript akan error kalau A → B → A.
  3. Hanya boleh satu prototype. Objek tidak bisa inherit dari dua objek sekaligus.
  4. Hati-hati dengan properti array/objek di prototype! Kalau prototype punya array, semua "anak" akan berbagi array yang sama (bukan salinan).
js
// BUG: Semua hamster berbagi perut yang sama!
const hamster = {
  perut: [],
  makan(makanan) {
    this.perut.push(makanan); // push ke prototype!
  }
};

const hamster1 = { __proto__: hamster };
const hamster2 = { __proto__: hamster };

hamster1.makan("apel");
console.log(hamster2.perut); // ["apel"] — Lho?!

// FIX: Setiap hamster punya perut sendiri
const hamster1Fix = { __proto__: hamster, perut: [] };
const hamster2Fix = { __proto__: hamster, perut: [] };
🎯Challenge

Buat 3 objek: kendaraan, mobil, sedan dengan rantai prototype:

  • kendaraan punya method klakson() yang print "BEEP!"
  • mobil inherit dari kendaraan, punya properti roda: 4
  • sedan inherit dari mobil, punya properti pintu: 4

Test: sedan.klakson() harus print "BEEP!" dan sedan.roda harus 4.


7.2 F.prototype

💡Analogi

Kalau __proto__ itu cara manual set warisan, F.prototype itu cara otomatis — setiap kali kamu pakai new, JavaScript otomatis set [[Prototype]] objek baru ke F.prototype.

Cara Kerja

js
const hewan = {
  makan: true
};

function Kelinci(nama) {
  this.nama = nama;
}

Kelinci.prototype = hewan;

const kelinci = new Kelinci("Putih");
// kelinci.__proto__ === hewan

console.log(kelinci.makan); // true

F.prototype hanya dipakai saat new F() dipanggil. Kalau F.prototype berubah setelahnya, objek yang sudah dibuat tidak terpengaruh.

Default: constructor

Setiap fungsi punya prototype default berisi { constructor: F }:

js
function Kelinci() {}

// Default:
// Kelinci.prototype = { constructor: Kelinci }

console.log(Kelinci.prototype.constructor === Kelinci); // true

const k = new Kelinci();
console.log(k.constructor === Kelinci); // true (dari prototype)

Ini berguna kalau kamu punya objek tapi tidak tahu constructor-nya:

js
const k2 = new k.constructor("Hitam"); // Bikin kelinci baru!
⚠️Jebakan!

Kalau kamu replace seluruh prototype, constructor hilang!

js
function Kelinci() {}

// SALAH: constructor hilang
Kelinci.prototype = {
  lompat: true
};

const k = new Kelinci();
console.log(k.constructor === Kelinci); // false!

// BENAR: tambahkan ke prototype yang ada
Kelinci.prototype.lompat = true;
// atau set ulang constructor:
Kelinci.prototype = {
  lompat: true,
  constructor: Kelinci
};
🎯Challenge

Buat constructor function Mobil(merk) yang:

  • Set this.merk = merk
  • Punya Mobil.prototype.info() yang return "Mobil: [merk]"
  • Pastikan constructor tetap benar setelah menambah method

7.3 Native Prototypes

💡Analogi

Pernah heran kenapa array punya .map(), .filter(), .push()? Padahal kamu tidak pernah bikin method itu. Jawabannya: semua method itu tinggal di Array.prototype, dan setiap array yang kamu buat otomatis mewarisinya.

Rantai Built-in

[1, 2, 3] → Array.prototype → Object.prototype → null "hello" → String.prototype → Object.prototype → null function() → Function.prototype → Object.prototype → null
js
const arr = [1, 2, 3];

console.log(arr.__proto__ === Array.prototype);        // true
console.log(arr.__proto__.__proto__ === Object.prototype); // true
console.log(arr.__proto__.__proto__.__proto__);         // null

Primitif Juga Punya Prototype

String, number, boolean — saat kamu akses method-nya, JavaScript bikin "wrapper object" sementara:

js
// Ini bisa karena String.prototype.toUpperCase ada
console.log("hello".toUpperCase()); // "HELLO"

// null dan undefined TIDAK punya prototype

Menambah Method ke Native Prototype (Polyfill)

js
// Polyfill: tambah method yang belum ada di engine lama
if (!String.prototype.repeat) {
  String.prototype.repeat = function(n) {
    return new Array(n + 1).join(this);
  };
}

console.log("ha".repeat(3)); // "hahaha"

PERINGATAN: Jangan sembarangan modifikasi native prototype! Ini bisa konflik dengan library lain. Satu-satunya alasan yang valid: polyfill (menambah fitur standar yang belum didukung engine).

Meminjam Method (Borrowing)

js
// Objek array-like (bukan array asli)
const obj = {
  0: "Hello",
  1: "World",
  length: 2
};

// Pinjam method join dari Array.prototype
obj.join = Array.prototype.join;
console.log(obj.join(", ")); // "Hello, World"
⚠️Jebakan!
  1. Jangan modifikasi Object.prototype! Semua objek mewarisinya — efeknya global dan berbahaya.
  2. Native prototype bisa di-override. Kalau dua library sama-sama menambah String.prototype.trim, yang terakhir menang.
🎯Challenge

Tambahkan method last() ke Array.prototype yang mengembalikan elemen terakhir array:

js
// Setelah kamu tambahkan:
console.log([1, 2, 3].last()); // 3
console.log(["a", "b"].last()); // "b"

7.4 Prototype Methods & Objects Tanpa proto

Cara Modern (Rekomendasi)

js
// Buat objek dengan prototype tertentu
const hewan = { makan: true };
const kelinci = Object.create(hewan);

console.log(kelinci.makan); // true
console.log(Object.getPrototypeOf(kelinci) === hewan); // true

// Ubah prototype (jarang dipakai, lambat!)
Object.setPrototypeOf(kelinci, {});

Object.create dengan Descriptor

js
const hewan = { makan: true };

const kelinci = Object.create(hewan, {
  lompat: {
    value: true,
    writable: true,
    enumerable: true,
    configurable: true
  }
});

console.log(kelinci.lompat); // true
console.log(kelinci.makan);  // true

Clone Sempurna (Termasuk Flag & Prototype)

js
const clone = Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

Objek "Sangat Polos" (Tanpa Prototype)

Berguna untuk dictionary/map yang aman dari key __proto__:

js
// Objek biasa: __proto__ itu "spesial"
const biasa = {};
biasa["__proto__"] = "test";
console.log(biasa["__proto__"]); // [object Object] — BUKAN "test"!

// Objek tanpa prototype: aman
const dict = Object.create(null);
dict["__proto__"] = "test";
console.log(dict["__proto__"]); // "test" — works!

// Tapi tidak punya toString, hasOwnProperty, dll
// Object.keys tetap works karena itu static method
console.log(Object.keys(dict)); // ["__proto__"]
⚠️Jebakan!
  1. Jangan ubah prototype setelah objek dibuat kecuali benar-benar perlu. Ini sangat lambat karena merusak optimisasi internal engine.
  2. Object.create(null) tidak punya method bawaan. Tidak bisa toString(), hasOwnProperty(), dll.
🎯Challenge

Buat "dictionary" yang aman menggunakan Object.create(null):

  • Tambahkan method toString yang non-enumerable (tidak muncul di for..in)
  • toString harus return semua key yang dipisahkan koma
js
// Harusnya:
dict.apple = "Apel";
dict.__proto__ = "test"; // Harus tersimpan sebagai data biasa!

for (let key in dict) console.log(key); // "apple", "__proto__"
console.log(dict.toString()); // "apple,__proto__"
// toString TIDAK muncul di loop

Sudah paham materi ini?

Tandai sebagai selesai untuk melacak progress-mu.