Soal 1draft berasal dari class yang tidak punya satu pun dari ketiga method. Apa yang terjadi saat kamu menjalankan const [firstLine] = draft;?
Special Method — toString, toJSON, dan Symbol.iterator
Dengan toString, toJSON, dan Symbol.iterator, yang dipanggil JavaScript secara otomatis, kamu bisa mengatur tampilan object sebagai string, memilih isi JSON, dan me-loop class dengan for...of.
Object pesanan tampil sebagai [object Object] di template literal, kehilangan total yang dihitung getter saat diubah dengan JSON.stringify, dan melempar TypeError di loop for...of.
Artikel ini membahas tiga special method (method yang dicari JavaScript berdasarkan namanya dan dipanggil dalam situasi tertentu), masing-masing untuk tampilan, konversi ke JSON, dan iterasi.
Mengatur Tampilan Object di Dalam String — toString
Misalkan kamu memasukkan pesanan ke teks notifikasi dengan menulis Diterima: ${order}. Yang muncul bukan nomor pesanan atau nominalnya, melainkan [object Object]. Kalau kamu menyusun ulang teksnya dari order.id dan order.total di setiap layar, kode penyusun string yang sama akan tersebar di mana-mana.
toString (method yang dipanggil JavaScript saat membutuhkan value dalam bentuk string; return value-nya dipakai sebagai string tersebut) dipanggil oleh ${}, String(), dan join milik array. Kalau class-mu tidak mendefinisikannya, yang dipakai adalah toString bawaan Object.prototype di bagian atas chain, yang mengembalikan [object Object].
// Class tanpa toString
class DraftOrder {
constructor(id, total) { this.id = id; this.total = total; }
}
// Class dengan toString
class Order {
constructor(id, total) { this.id = id; this.total = total; }
toString() { return `Pesanan ${this.id} (${this.total} yen)`; }
}
const draft = new DraftOrder("A-101", 3200);
const order = new Order("A-102", 4800);
// Memasukkannya ke template literal akan memanggil toString
console.log(`Diterima: ${draft}`); // Diterima: [object Object]
console.log(`Diterima: ${order}`); // Diterima: Pesanan A-102 (4800 yen)
// String() dan join milik array memakai toString yang sama
console.log(String(order)); // Pesanan A-102 (4800 yen)
console.log([order, new Order("A-103", 1500)].join(" / ")); // Pesanan A-102 (4800 yen) / Pesanan A-103 (1500 yen)
Kode pemanggilnya, ${}, sama persis di kedua baris. Untuk mengubah format tampilan, kamu cukup mengubah satu tempat, yaitu toString di class, dan semua baris yang menyisipkan object itu ikut berubah. Kalau instance diberikan langsung, seperti console.log(order), toString tidak dipanggil.
Menyertakan Total di JSON yang Dikirim — toJSON
Untuk mengirim pesanan ke server, kamu mengubahnya dengan JSON.stringify(order). Kalau class-nya menyimpan item pesanan di #lines dan menghitung total lewat getter total, keduanya bukan property biasa milik instance, sehingga yang terkirim hanya {"id":"A-102"}.
Kalau kamu menulis toJSON (method yang dipanggil JSON.stringify sebelum mengubah sebuah value; yang diubah menjadi JSON adalah return value-nya), kamu bisa mengembalikan object yang berisi tepat value yang ingin kamu kirim. Date berubah menjadi string tanggal dan waktu di JSON karena Date punya method ini.
class Order {
#lines; // Item pesanan (tidak bisa dibaca dari luar)
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
// Dipanggil oleh JSON.stringify; return value-nya yang diubah menjadi JSON
toJSON() { return { id: this.id, itemCount: this.#lines.length, total: this.total }; }
}
const order = new Order("A-102", [{ price: 1200, quantity: 2 }, { price: 2400, quantity: 1 }]);
const other = new Order("A-103", [{ price: 1500, quantity: 1 }]);
// Satu-satunya key milik instance adalah id
console.log(Object.keys(order).join(", ")); // id
// Yang diubah menjadi JSON adalah return value toJSON
console.log(JSON.stringify(order)); // {"id":"A-102","itemCount":2,"total":4800}
// Di dalam array, toJSON dipanggil untuk setiap element
console.log(JSON.stringify([order, other]));
// [{"id":"A-102","itemCount":2,"total":4800},{"id":"A-103","itemCount":1,"total":1500}]
#lines tetap tidak bisa dibaca dari luar; class itu sendiri yang memilih value mana yang masuk ke JSON. Bahkan value yang tidak disimpan instance sebagai property, seperti itemCount, bisa muncul di JSON kalau kamu menambahkannya ke object yang dikembalikan. Kalau string itu kamu parse kembali dengan JSON.parse, hasilnya object biasa dengan bentuk ini, bukan instance Order.
Kalau toJSON Mengembalikan String, String Itu Diubah Dua Kali
Kalau kamu mengembalikan string, seperti return JSON.stringify({ id: this.id, total: this.total });, string itu diubah lagi menjadi JSON. Setelah satu kali JSON.parse pun, hasilnya masih string, dan .total bernilai undefined. Kembalikan object saja.
Me-loop Item Pesanan dengan for...of — Symbol.iterator
Berikutnya, kamu ingin menampilkan item pesanan di layar, satu baris per item. Item-item itu disembunyikan di #lines, jadi tidak bisa dibaca dari luar, dan menulis for (const line of order) melempar TypeError: order is not iterable. Walaupun item-nya kamu simpan di array publik, kamu tetap tidak bisa melakukan loop pada instance itu sendiri.
Kalau kamu mendefinisikan method dengan nama Symbol.iterator (value bawaan yang unik, dipakai sebagai nama method yang dipanggil for...of), for...of mengambil value satu per satu dengan memanggil next() pada object yang dikembalikan method itu.
Di dalam class, generator method function* nama() ditulis sebagai *nama(). Menulis nama di dalam kurung siku, seperti [Symbol.iterator], membuat value di dalam kurung itu dipakai sebagai nama method.
class Order {
#lines; // Item pesanan (tidak bisa dibaca dari luar)
constructor(id, lines) { this.id = id; this.#lines = lines; }
// Method yang dipanggil for...of. Setiap item yang di-yield masuk ke variabel loop
*[Symbol.iterator]() {
for (const line of this.#lines) yield line;
}
}
const order = new Order("A-102", [{ name: "Kopi drip", quantity: 2 }, { name: "Mug", quantity: 1 }]);
// Ambil item pesanan satu per satu dari luar class
for (const line of order) {
console.log(`${line.name} × ${line.quantity}`); // 2 baris: Kopi drip × 2 / Mug × 1
}
// Setiap spread memanggil method itu lagi, jadi yang kedua juga mendapat 2 item
console.log([...order].length, [...order].length); // 2 2
Generator yang sudah dibaca sampai habis akan tetap kosong, seperti yang dibahas di artikel iterator dan generator. Pemanggil hanya mendapat item yang di-yield, tidak pernah array #lines itu sendiri, jadi pemanggil tidak bisa menambah atau menghapus item pesanan dari luar.
Mengembalikan Array Langsung Membuat for...of Gagal
Kalau kamu mengembalikan array, seperti return this.#lines;, array tidak punya next(), jadi for...of melempar TypeError: undefined is not a function. Array memang iterable, tetapi bukan iterator. Tambahkan *, lalu yield setiap element-nya.
Melacak Method yang Lupa Ditulis dari Hasilnya — Ketiganya Sekaligus
Kalau tampilan atau JSON tidak keluar sesuai harapan, baris seperti ${order} dan JSON.stringify(order) tidak menyebut nama method yang dipanggilnya. Untuk mengetahui method mana yang lupa kamu tulis, telusuri balik dari cara pemanggilannya dan bentuk hasilnya.
Pada instance dari class yang tidak punya satu pun dari ketiga method itu, setiap bentuk pemanggilan tetap mencari nama method-nya masing-masing di sepanjang prototype chain. Satu-satunya yang ditemukan adalah toString bawaan di Object.prototype; toJSON dan [Symbol.iterator] tidak ditemukan walaupun sudah dicari sampai null.
// Class yang tidak punya toString, toJSON, maupun [Symbol.iterator]
class DraftOrder {
#lines;
constructor(id, lines) { this.id = id; this.#lines = lines; }
get total() { return this.#lines.reduce((sum, line) => sum + line.price * line.quantity, 0); }
}
const draft = new DraftOrder("A-101", [{ name: "Kopi drip", price: 1200, quantity: 2 }]);
// toString ditemukan di Object.prototype; toJSON tidak ada, jadi hanya key miliknya sendiri yang ditampilkan
console.log(`${draft}`); // [object Object]
console.log(JSON.stringify(draft)); // {"id":"A-101"}
// [Symbol.iterator] tidak ada di mana pun, jadi error dilempar
try {
console.log([...draft]);
} catch (error) {
console.log(error.message); // draft is not iterable
}
JSON.stringify tidak melempar error; hasilnya {"id":"A-101"}, jadi data tanpa item #lines dan total bisa terkirim tanpa disadari sampai pihak penerima menemukannya. Tabel di bawah merangkum bentuk lain yang mencari nama yang sama, beserta hasilnya kalau method itu tidak ditulis.
| Cara memanggil | Method yang dicari | Hasil tanpa method |
|---|---|---|
| JSON.stringify([draft]) | toJSON (per element) | [{"id":"A-101"}] |
| for (const line of draft) | [Symbol.iterator] | TypeError: draft is not iterable |
| const [firstLine] = draft | [Symbol.iterator] | TypeError: draft is not iterable |
Cek Pemahaman
Jawab setiap pertanyaan satu per satu.
Soal 2Array berisi dua pesanan yang toJSON-nya mengembalikan { id: this.id } diberikan ke JSON.stringify. Apa hasilnya?
Soal 3Kalau kamu menulis [...cart] dua kali untuk cart yang punya *[Symbol.iterator](), berapa panjang array yang kedua?