Rangkuman OOP — Tabel Ringkas Sintaks Class dan this

Tabel ringkas sintaks class di kategori ini: tempat member disimpan, urutan pencarian nama, cara this ditentukan, tempat menambah fitur, dan kapan error menghentikan kode.

Di kode manajemen tugas, member static dan #, child class yang dibuat dengan extends, dan method yang diambil dari object semuanya bercampur di satu file. Kalau perbedaannya belum kamu pahami, kamu tidak bisa menelusuri dari mana undefined atau TypeError berasal.

Artikel ini mengulas sintaks di kategori ini lewat tabel ringkas, dengan fokus pada sintaks class dan this.

Membaca Member dari Tempat Penyimpanannya — Tabel Ringkas Sintaks Class

Misalkan kamu sedang membaca class Task yang ditulis orang lain. Baris dengan static, #, dan get berjajar di dalam kurung kurawal yang sama, dan dengan membacanya baris per baris pun, kamu tidak akan tahu kenapa task.fromJSON(text) melempar TypeError atau kenapa status selesai tidak muncul di JSON.stringify(task).

Cara kamu menulis sesuatu di body class menentukan tempat penyimpanannya: di instance itu sendiri, di Task.prototype (tempat method yang dipakai bersama semua instance), atau di class itu sendiri. Saat sebuah nama dibaca, JavaScript mencarinya di instance itu sendiri, lalu Task.prototype, lalu Object.prototype.

class Task {
  static count = 0;                              // Hanya Task sendiri yang punya
  priority = "normal";                           // Ditambahkan ke setiap tugas di setiap new
  #done = false;                                 // Disimpan di task, tetapi tidak bisa dibaca dari luar class
  constructor(title) {
    this.title = title;
    Task.count += 1;
  }
  complete() { this.#done = true; }              // Ditaruh di Task.prototype, dipakai bersama semua instance
  get status() { return this.#done ? "selesai" : "belum mulai"; }
  static fromJSON(text) { return new Task(JSON.parse(text).title); }
}

// Cek nama mana yang bisa dan tidak bisa dibaca dari instance
const task = Task.fromJSON('{"title":"Kirim penawaran"}');
task.complete();
console.log(task.status, Task.count);                                 // selesai 1
console.log(Object.keys(task).join(", "));                            // priority, title
console.log(JSON.stringify(task));                                    // {"priority":"normal","title":"Kirim penawaran"}
console.log(task.hasOwnProperty("complete"), typeof task.fromJSON);   // false undefined
Nama yang Bisa Dicari dari task
Dicari lewat task.nama
  • Dicari berurutan: task sendiri → Task.prototype → Object.prototype
task sendiri
  • title, priority — muncul sebagai key dan masuk ke JSON
Task.prototype (bersama)
  • complete(), get status() — ditemukan saat dicari
Private field di luar jangkauan task.nama
  • #done — disimpan di task, tetapi hanya bisa dibaca sebagai this.#done di dalam class
Task sendiri (static)
  • count, fromJSON() — di luar jangkauan pencarian
  • task.fromJSON bernilai undefined
Nama # dan member static berada di luar jangkauan pencarian task. task.fromJSON tidak ditemukan, jadi panggil lewat nama class.

status ada di Task.prototype, dan #done ada di task tetapi bukan key biasa, jadi keduanya tidak muncul di Object.keys maupun JSON.stringify. Untuk memasukkannya ke JSON, kembalikan value yang kamu inginkan dari toJSON. Tabel di bawah mengelompokkan sintaks di kategori ini berdasarkan tempat penyimpanan setiap member.

SintaksTempatnyaDibaca dari task
this.title di constructortask sendiriAda di key dan JSON
Class field prioritytask sendiriAda di key dan JSON
Private field #donetask sendiri (hanya class)Tidak ada di key dan JSON
Method complete()Task.prototypeBisa dipanggil, bukan key
get / set statusTask.prototypeDibaca tanpa ()
static count / fromJSON()Task sendiriundefined

Menelusuri Definisi yang Berjalan — extends dan super

Ambil contoh tugas mingguan berulang yang dibuat dengan class RecurringTask extends Task. Dari baris weekly.describe() saja, kamu tidak bisa tahu apakah describe milik child class atau milik parent yang berjalan, atau kenapa kamu bisa memanggil complete padahal child class tidak mendefinisikannya.

Sebuah nama dicari di sepanjang prototype chain, mulai dari object itu sendiri, dan definisi pertama yang ditemukan itulah yang berjalan. Override di child class ditemukan lebih dulu daripada milik parent, dan super.describe() mulai mencari dari parent. Method yang ditulis terpisah untuk setiap jenis dengan polymorphism juga ditemukan dengan urutan yang sama.

class Task {
  constructor(title) { this.title = title; }
  describe() { return `Tugas: ${this.title}`; }
  complete() { return `Selesai: ${this.title}`; }
}

// Child class: memanggil constructor parent dengan super() dan hanya meng-override describe
class RecurringTask extends Task {
  constructor(title, weekday) { super(title); this.weekday = weekday; }
  describe() { return `${super.describe()} (setiap ${this.weekday})`; }
}

// Panggil untuk melihat definisi mana yang berjalan
const weekly = new RecurringTask("Kirim laporan mingguan", "Jumat");
console.log(weekly.describe());                              // Tugas: Kirim laporan mingguan (setiap Jumat)
console.log(weekly.complete());                              // Selesai: Kirim laporan mingguan
console.log(`${weekly}`);                                    // [object Object]
console.log(weekly instanceof Task, typeof weekly.archive);  // true undefined
Urutan Tingkat yang Dicari dari weekly
weekly(instance)Punya titledan weekdayRecurringTask.prototypedescribe ditemukan← weekly.describe()Task.prototypecomplete ditemukan← super.describe()Object.prototypetoString ditemukan← ${weekly}
describe ditemukan di tingkat kedua dan complete di tingkat ketiga. describe milik parent hanya berjalan kalau kamu memanggilnya lewat super.

Nama yang tidak ada di tingkat mana pun, seperti archive, bernilai undefined, dan memanggilnya melempar TypeError. ${weekly} menghasilkan [object Object] karena toString bawaan di tingkat terakhir yang berjalan.

Membedakan this dari Cara Function Dipanggil — Tabel Ringkas this

Sekarang misalkan kamu menyimpan method penyelesaian tugas di sebuah variabel untuk dipanggil nanti, atau memberikannya ke setTimeout. Method yang berjalan normal sebagai task.complete() bisa mendapat this yang bukan task, tergantung cara kamu mengopernya, lalu melempar TypeError atau menghasilkan this.title bernilai undefined.

Kapan value this ditentukan bergantung pada cara function itu ditulis. Untuk method biasa di class, this ditentukan oleh apa yang ada di kiri titik pada baris yang memanggilnya; untuk field arrow function, this ditentukan saat new membuat instance. Yang dikembalikan bind adalah function terpisah yang this-nya dikunci ke value yang kamu berikan ke bind.

class Task {
  onComplete = () => `Selesai: ${this.title}`;    // this adalah tugas yang dibuat new
  constructor(title) { this.title = title; }
  complete() { return `Selesai: ${this.title}`; } // this ditentukan oleh baris pemanggil
}
const task = new Task("Kirim tagihan");

// Panggil dengan task di kiri titik
console.log(task.complete());   // Selesai: Kirim tagihan

// Simpan di variabel, lalu panggil
const runMethod = task.complete;
const runArrow = task.onComplete;
const runBound = task.complete.bind(task);
console.log(runArrow());        // Selesai: Kirim tagihan
console.log(runBound());        // Selesai: Kirim tagihan
// runMethod();                 // TypeError: Cannot read properties of undefined (reading 'title')
Cara this Ditentukan untuk Function yang Diambil
const runMethod= task.completeMethod biasa:diatur saatdipanggilrunMethod(): tidakada object di kirititikthis undefinedTypeErrorconst runArrow= task.onCompleteField arrowfunction: diatursaat newDi runArrow() punthis tetap taskSelesai: Kirimtagihan
this di runMethod() bernilai undefined. Kalau kamu menulisnya seperti baris bawah, function yang diambil dan dioper tetap memegang task.

runBound di kode itu juga tetap memakai task sebagai this setelah diambil. Dengan setTimeout(task.complete, 100), browser mengisi this dengan window, jadi bungkus dengan arrow function. Tabel di bawah menunjukkan kapan this ditentukan untuk setiap cara pemanggilan.

Cara dipanggilKapan this ditentukanSaat diambil dan dioper
Method biasaKiri titik saat dipanggiltask hilang, TypeError
Return value bindSaat bind dipanggilTetap task
Field arrow functionSaat newTetap task
Dibungkus () => task.complete()Saat task.complete() dipanggilTetap task
Method dari Object.assignKiri titik saat dipanggilObject target hilang

Method Child Class dengan Nama yang Sama Tidak Pernah Berjalan

Walaupun kamu menulis class RecurringTask extends Task { onComplete() { … } }, field arrow function dari parent Task ditaruh di weekly itu sendiri dan ditemukan lebih dulu, jadi weekly.onComplete() menjalankan function milik parent. Kalau child class perlu meng-override-nya, definisikan juga sebagai method di parent.

Memilih Tempat Menambah Fitur — Inheritance, prototype, atau Composition

Misalkan kamu menambahkan fitur ke aplikasi manajemen tugas yang sudah berjalan: menyematkan satu tugas saja, menampilkan label di semua tugas yang sudah ada, atau menambahkan penanggung jawab hanya ke tugas tim. Kalau fiturnya kamu tulis di tempat yang salah, tugas yang kamu kira sudah punya fitur itu malah melempar TypeError.

Cara menambah fitur yang dibahas di kategori ini berbeda dalam hal tempat fitur itu ditulis: di instance itu sendiri, di Task.prototype, atau di tingkat baru yang disambungkan dengan extends. Begitu kamu tahu tempat fitur itu ditulis, kamu juga tahu instance mana yang bisa menemukan nama itu di sepanjang chain-nya.

class Task {
  constructor(title) { this.title = title; }
}
const first = new Task("Kirim penawaran");          // Buat 2 tugas lebih dulu
const second = new Task("Tulis notulen rapat");

// Salin ke satu tugas saja
Object.assign(first, { pin() { return `Disematkan: ${this.title}`; } });
// Assign ke Task.prototype
Task.prototype.label = function () { return `Tugas: ${this.title}`; };
// Buat class dengan tingkat tambahan dari mixin
const Assignable = (Base) => class extends Base {
  assign(name) { return `Ditugaskan ke ${name}: ${this.title}`; }
};
class TeamTask extends Assignable(Task) {}

console.log(typeof second.pin, second.label());          // undefined Tugas: Tulis notulen rapat
console.log(typeof first.assign);                        // undefined
console.log(new TeamTask("Kirim laporan mingguan").assign("Budi"));   // Ditugaskan ke Budi: Kirim laporan mingguan
Tempat Fitur Ditulis Menentukan Jangkauannya
first dan seconddibuat dari TaskObject.assign(first, komponen)Task.prototype.label = functionextendsAssignable(Task)Masuk ke keymilik firstDitambahkan sekalike Task.prototypeMenambah tingkatdi atas TeamTaskHanya first;second undefinedKedua tugas yangada bisa memanggilHanya TeamTaskbaru, bukanfirst/second
Kalau ditulis di first sendiri, fitur hanya sampai ke satu tugas itu; kalau di Task.prototype, fitur sampai ke kedua tugas yang sudah ada. Mixin hanya sampai ke instance class baru.

label sampai ke second, yang sudah ada sebelumnya, karena Task.prototype ada di chain milik second. Assign ke prototype dipakai untuk class yang body-nya tidak bisa kamu ubah. Method yang ditulis di child class, seperti mixin, hanya sampai ke instance yang dibuat dari child class itu.

Pakai typeof untuk Mengecek Fitur yang Ditambahkan ke Object Tertentu

Walaupun Object.assign hanya menyalin pin ke first, instanceof Task tetap true untuk first maupun second. Karena keduanya tidak bisa dibedakan lewat class, pastikan method-nya ada dengan typeof task.pin === "function" sebelum memanggilnya.

Mencari Penyebab dari Saat Kode Berhenti — Tabel Ringkas Error

Saat kode class melempar error, baris tempat kode berhenti dan baris yang perlu kamu perbaiki bisa berjauhan. Kesalahan di constructor child class melempar error dari baris di dalamnya saat new dijalankan, dan kalau nama # ditulis di luar class, output console.log dari baris di atasnya pun tidak muncul.

Error terbagi menjadi tiga kelompok berdasarkan kapan kode berhenti: saat kode dimuat, saat new menjalankan constructor, atau saat method dipanggil. Dengan SyntaxError saat pemuatan, tidak ada satu baris pun yang berjalan; pada dua lainnya, baris sebelum error berjalan lebih dulu, baru kemudian kode berhenti.

class Task {
  #done = false;
  constructor(title) { this.title = title; }
  complete() { return `Selesai: ${this.title}`; }
}
class RecurringTask extends Task {
  constructor(title, weekday) {
    this.weekday = weekday;   // Memakai this sebelum super()
    super(title);
  }
}
console.log("Mulai pengecekan");   // Tampil pada ② dan ③, tetapi tidak kalau ada baris ①

// ① Saat dimuat: console.log(new Task("Kirim tagihan").#done);
//    SyntaxError: Private field '#done' must be declared in an enclosing class
// ② Saat new: new RecurringTask("Kirim laporan mingguan", "Jumat");
//    ReferenceError: Must call super constructor in derived class before accessing 'this' …
// ③ Saat dipanggil: const run = new Task("Kirim tagihan").complete; run();
//    TypeError: Cannot read properties of undefined (reading 'title')
Tiga Titik Saat Error Menghentikan Kode
① Saat kodedimuatSyntaxErrorTidak ada barisyang berjalan② newmenjalankanconstructorReferenceErrorBaris di atas newberjalan③ MethoddipanggilTypeErrorBaris sebelumpemanggilanberjalan
Makin atas barisnya, makin awal kode berhenti; pada ①, pesan pengecekan pun tidak muncul. Kalau tidak ada output sama sekali, curigai SyntaxError saat pemuatan.

② dan ③ menampilkan semua output sampai titik itu sebelum berhenti, jadi kamu bisa mencari penyebabnya tepat setelah output terakhir. Tabel di bawah merangkum error yang muncul di kategori ini, diurutkan dari titik berhenti yang paling awal.

Pesan errorPenyebabCara memperbaiki
SyntaxError: Private fieldtask.#done di luar classBaca lewat getter
SyntaxError: … export namedDefault export dibaca { Cart }Hapus kurung kurawal
ReferenceError: Must call superthis sebelum super()Panggil super() dulu
TypeError: … without 'new'class dipanggil tanpa newTambahkan new
TypeError: Cannot read propertiesMethod diambil dipanggil run()bind atau bungkus arrow
RangeError: Maximum call stacksetter assign nama sama / lupa super.Ganti nama / tambah super.
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1RecurringTask tidak mendefinisikan complete. Di tingkat mana definisi untuk weekly.complete() ditemukan?

Soal 2Kalau field arrow function kamu panggil sebagai const run = task.onComplete; run();, apa isi this?

Soal 3Baris new yang ditulis setelah console.log melempar ReferenceError. Apa yang terjadi pada output console.log itu?