Soal 1RecurringTask tidak mendefinisikan complete. Di tingkat mana definisi untuk weekly.complete() ditemukan?
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
- Dicari berurutan: task sendiri → Task.prototype → Object.prototype
title,priority— muncul sebagai key dan masuk ke JSON
complete(),get status()— ditemukan saat dicari
#done— disimpan di task, tetapi hanya bisa dibaca sebagaithis.#donedi dalam class
count,fromJSON()— di luar jangkauan pencariantask.fromJSONbernilaiundefined
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.
| Sintaks | Tempatnya | Dibaca dari task |
|---|---|---|
| this.title di constructor | task sendiri | Ada di key dan JSON |
| Class field priority | task sendiri | Ada di key dan JSON |
| Private field #done | task sendiri (hanya class) | Tidak ada di key dan JSON |
| Method complete() | Task.prototype | Bisa dipanggil, bukan key |
| get / set status | Task.prototype | Dibaca tanpa () |
| static count / fromJSON() | Task sendiri | undefined |
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
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')
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 dipanggil | Kapan this ditentukan | Saat diambil dan dioper |
|---|---|---|
| Method biasa | Kiri titik saat dipanggil | task hilang, TypeError |
| Return value bind | Saat bind dipanggil | Tetap task |
| Field arrow function | Saat new | Tetap task |
| Dibungkus () => task.complete() | Saat task.complete() dipanggil | Tetap task |
| Method dari Object.assign | Kiri titik saat dipanggil | Object 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
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')
② 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 error | Penyebab | Cara memperbaiki |
|---|---|---|
| SyntaxError: Private field | task.#done di luar class | Baca lewat getter |
| SyntaxError: … export named | Default export dibaca { Cart } | Hapus kurung kurawal |
| ReferenceError: Must call super | this sebelum super() | Panggil super() dulu |
| TypeError: … without 'new' | class dipanggil tanpa new | Tambahkan new |
| TypeError: Cannot read properties | Method diambil dipanggil run() | bind atau bungkus arrow |
| RangeError: Maximum call stack | setter assign nama sama / lupa super. | Ganti nama / tambah super. |
Cek Pemahaman
Jawab setiap pertanyaan satu per satu.
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?