Belajar dengan membaca secara berurutan

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)
Di Mana ${} Menemukan toString
Diterima: ${draft}DraftOrder.prototypetidak punyatoStringDitemukan diObject.prototypeDiterima:[object Object]Diterima: ${order}Ditemukan diOrder.prototypeMemanggiltoString milikOrderDiterima: PesananA-102 (4800 yen)
Kedua instance tidak punya toString sendiri, jadi JavaScript mencarinya di chain. Kalau class mendefinisikannya, toString itu ditemukan lebih dulu daripada milik Object.prototype.

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.

Kamu akan menampilkan daftar bus yang berangkat di papan informasi depan stasiun. BusDeparture dan departures sudah dideklarasikan.

① Tulis method yang mengembalikan teks dalam bentuk “Rute 42 [Jalur 2] tujuan Rumah Sakit Kota”.

② Masukkan departures[0] ke ${} dan tampilkan setelah “Segera berangkat: ”.

③ Gabungkan departures dengan “ / ” tanpa mengubah object-nya lebih dulu, lalu tampilkan hasilnya.

④ Ubah jalur bus kedua menjadi 3, lalu masukkan ke ${} dan tampilkan.

(Kalau berjalan dengan benar, penjelasan akan muncul.)

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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}]
Pengaruh toJSON dan Array pada Hasilnya
Diberikan keJSON.stringifyorder tanpatoJSONorder dengantoJSON[order, other](dengan toJSON)Hanya membaca id,satu-satunya keyMembaca returnvalue toJSON()Memanggil toJSON()untuk setiapelement{"id":"A-102"}{"id":"A-102",…"total":4800}[{…"total":4800},{…"total":1500}]
Tanpa toJSON, yang dibaca hanya id, satu-satunya key milik instance. Dengan toJSON, yang diubah adalah return value-nya, dan toJSON juga dipanggil untuk setiap element array.

#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.

Kamu akan mengirim data member ke server sebagai JSON tanpa menyertakan nomor kartunya. Member, member, dan sentAt sudah dideklarasikan.

① Tulis method yang hanya mengembalikan ID member dan nama saat object diubah ke JSON.

② Ubah member langsung menjadi string JSON dan tampilkan.

③ Gabungkan sentAt dan member menjadi satu object, ubah menjadi string JSON, lalu tampilkan.

④ Tampilkan apakah string dari ③ berisi nomor kartu.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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 Baru Setiap Kali
for (const lineof order)[Symbol.iterator]()→ menampilkan2 baris[...order]yang pertamaDipanggil lagi,jadi 2 item[...order]yang keduaDipanggil lagi,jadi tetap 2 item
Ketiga bentuk ini masing-masing memanggil [Symbol.iterator]() lagi. Spread kedua pun mulai dari awal dan mendapat kedua item.

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.

Kamu akan membuat keranjang belanja yang bisa di-loop tanpa membuka array internalnya. Cart dan cart sudah dideklarasikan.

① Buat produk bisa diambil satu per satu dengan for...of.

② Loop cart langsung dengan for...of, lalu tampilkan setiap produk yang jumlahnya 1 atau lebih sebagai “nama × jumlah”.

③ Ubah cart langsung menjadi array dengan spread syntax, lalu tampilkan total harga sebagai “Total: ... yen”.

④ Lakukan destructuring pada cart untuk mengambil produk pertama, lalu tampilkan sebagai “Pertama: nama”.

JavaScript / TypeScript Editor

Jalankan kode untuk melihat output

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
}
Hasil untuk draft yang Tidak Punya Ketiga Method
${draft}JSON.stringify(draft)[...draft]Mencari toStringMencari toJSONMencari[Symbol.iterator]Ditemukan diObject.prototypeTidak ditemukan,bahkan sampai nullTidak ditemukan,bahkan sampai null[object Object]{"id":"A-101"}(tanpa total)TypeError: draftis not iterable
Ketiga bentuk masing-masing mencari nama yang berbeda di chain. Hanya [Symbol.iterator] yang melempar error; dua lainnya mengembalikan hasil bawaan.

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 memanggilMethod yang dicariHasil 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
QUIZ

Cek Pemahaman

Jawab setiap pertanyaan satu per satu.

Soal 1draft berasal dari class yang tidak punya satu pun dari ketiga method. Apa yang terjadi saat kamu menjalankan const [firstLine] = draft;?

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?