Menu

Iterator dan Generator JavaScript: function*, yield, Symbol.iterator

Cara kerja iterator protocol JavaScript, cara membuat objekmu sendiri iterable, dan bagaimana generator function membuat semuanya jadi mudah.

Halaman ini dilengkapi editor yang bisa dijalankan. Edit, jalankan, dan lihat output-nya seketika.

Iterator Protocol

Banyak fitur JavaScript, seperti for...of, spread (...), destructuring, Array.from, dan Promise.all, memakai satu mekanisme dasar yang sama: iterator protocol. Begitu kamu memahaminya, semuanya terlihat seperti variasi dari gagasan yang sama.

Iterator adalah objek apa pun yang punya method next() yang mengembalikan { value, done }:

Panggil next() berulang kali. Setiap panggilan mengembalikan nilai berikutnya dan sebuah flag done. Saat done bernilai true, urutannya selesai. Itulah seluruh protokolnya: empat huruf dan sebuah boolean.

Iterable vs Iterator

Ada konsep kedua yang berkaitan. Iterable adalah apa pun yang tahu cara menghasilkan iterator. Ia melakukannya lewat method yang disimpan di bawah key khusus: Symbol.iterator.

Array adalah iterable. Memanggil numbers[Symbol.iterator]() mengembalikan iterator baru. String, Map, Set, dan arguments juga iterable, dan itulah sebabnya for...of bekerja pada semuanya.

Pembagian ini penting: iterable adalah koleksinya, iterator adalah kursornya. Kamu bisa meminta sebanyak apa pun kursor independen dari sebuah iterable.

Mengapa for...of Berfungsi

for...of hanyalah sintaks pemanis di atas iterator protocol. Di baliknya, ia memanggil Symbol.iterator, lalu next() sampai done bernilai true:

Spread dan destructuring melakukan hal yang sama: keduanya menelusuri iterator sampai selesai:

Objek apa pun yang kamu bangun dengan menerapkan Symbol.iterator otomatis bisa ikut dipakai di semua fitur ini.

Mari buat objek range yang menghasilkan angka dari start sampai end:

Beberapa hal yang perlu diperhatikan:

  • [Symbol.iterator]() memakai nama method terhitung. Key-nya adalah symbol itu sendiri, bukan string "Symbol.iterator".
  • Setiap panggilan ke [Symbol.iterator]() mengembalikan iterator yang benar-benar baru dengan current miliknya sendiri. Itulah yang memungkinkanmu melakukan perulangan pada range dua kali tanpa ia "habis terpakai".
  • Iterator yang dikembalikan hanya butuh next(). Itu saja.

Ini berfungsi, tetapi panjang. Ada cara yang jauh lebih baik.

Kenalan dengan Generator

Generator function dideklarasikan dengan function* (perhatikan bintangnya). Alih-alih berjalan sampai selesai, ia bisa berhenti sejenak di ekspresi yield dan dilanjutkan nanti. Memanggilnya tidak menjalankan isi fungsinya, melainkan menyerahkan objek generator yang sekaligus iterator dan iterable:

Setiap next() menjalankan isi fungsi sampai bertemu yield, berhenti sejenak, lalu mengembalikan { value, done: false }. Saat fungsinya selesai, kamu mendapat { value: undefined, done: true }.

Dan karena generator adalah iterable, ia bekerja dengan semua fitur dari bagian sebelumnya:

Bandingkan versi panjang di atas dengan ini:

Selesai. * di depan [Symbol.iterator] menjadikannya method generator. yield i menggantikan seluruh objek iterator yang ditulis manual. Tanpa next, tanpa done, tanpa risiko selisih satu: hanya loop biasa dengan yield alih-alih push.

Inilah alasan generator ada. Generator mengubah "tulis sebuah iterator" menjadi "tulis fungsi yang melakukan yield".

yield vs return

yield menjeda; return mengakhiri. Kamu bisa melakukan yield sebanyak yang kamu mau, dan generator dilanjutkan dari titik terakhir:

return di dalam generator muncul sebagai { value: "done", done: true } pada panggilan yang mengakhirinya. for...of dan spread mengabaikan nilai yang dikembalikan itu, karena keduanya hanya memakai item yang done-nya bernilai false. Jadi jangan pakai return value untuk menyelundupkan item terakhir ke dalam loop; item itu akan dilewati.

Urutan Lazy dan Tanpa Akhir

Generator menghasilkan nilai sesuai permintaan, satu per satu. Artinya kamu bisa mewakili urutan yang mustahil dibuat sebagai array:

Loop-nya benar-benar while (true), tetapi programnya tetap selesai, karena generator hanya maju saat ada yang meminta nilai berikutnya. Kamu bisa mengambil N item pertama, berhenti, dan sisanya tidak pernah berjalan:

take sendiri adalah generator yang membungkus generator lain. Menyusun generator seperti ini adalah bagian besar dari daya tariknya: potongan-potongan kecil, masing-masing melakukan satu hal.

Mendelegasikan dengan yield*

Kalau sebuah generator perlu melakukan yield untuk semua isi dari iterable lain, yield* mendelegasikannya:

yield* bekerja dengan iterable apa pun, seperti array, set, atau generator lain, dan meneruskan setiap item satu per satu. Ini padanan spread untuk iterator.

Sekilas tentang Async Generator

Generator yang dideklarasikan dengan async function* bisa melakukan yield pada nilai yang butuh waktu, berguna untuk streaming dari API atau membaca file sepotong demi sepotong. Kamu memakainya dengan for await...of:

async function* paginate(url) {
  let next = url;
  while (next) {
    const res = await fetch(next);
    const page = await res.json();
    for (const item of page.items) yield item;
    next = page.nextUrl;
  }
}

for await (const item of paginate("/api/users")) {
  console.log(item);
}

Potongan kode ini tidak bisa dijalankan di sini (butuh endpoint sungguhan), tetapi bentuknya layak kamu ketahui. Begitu kamu memahami generator biasa, async generator adalah gagasan yang sama dengan taburan await.

Kapan Memakai Generator

Pakai generator saat:

  • Urutannya tanpa akhir atau berpotensi tanpa akhir, seperti ID, timestamp, atau jeda retry.
  • Menghasilkan semua nilai itu mahal dan pemakainya mungkin berhenti lebih awal.
  • Kamu menerapkan Symbol.iterator pada objek buatanmu sendiri. Hampir selalu lebih singkat daripada menulis objek { next() } secara manual.
  • Kamu ingin menyusun transformasi streaming (take, filter, map) tanpa membuat array perantara.

Pakai array biasa saat datanya sudah ada di memori dan ukurannya kecil. Generator tidak gratis: mesin yang menjeda dan melanjutkan fungsi punya overhead, dan stack trace yang melewati kode generator bisa lebih sulit dibaca.

Berikutnya: Symbol

Symbol.iterator adalah symbol pertama yang dikenal kebanyakan orang, tetapi jauh dari satu-satunya. Symbol adalah tipe primitif yang dirancang persis untuk titik perluasan seperti ini: key unik yang memungkinkan bahasa dan kodemu sendiri terhubung ke objek tanpa bentrok dengan nama property biasa. Itu topik halaman berikutnya.

Pertanyaan yang sering diajukan

Apa bedanya iterable dan iterator di JavaScript?

Iterable adalah objek apa pun yang punya method Symbol.iterator yang mengembalikan iterator. Iterator adalah objek yang benar-benar menghasilkan nilai: ia punya method next() yang mengembalikan { value, done }. Array, string, Map, dan Set adalah iterable; memanggil method Symbol.iterator miliknya memberimu iterator yang bisa kamu telusuri langkah demi langkah.

Apa itu generator function di JavaScript?

Fungsi yang dideklarasikan dengan function* dan menghasilkan nilai secara lazy dengan yield. Memanggilnya tidak menjalankan isi fungsinya, melainkan mengembalikan objek generator yang sekaligus iterator dan iterable. Setiap panggilan next() berjalan sampai yield berikutnya, berhenti sejenak, lalu mengembalikan nilai yang di-yield.

Apa bedanya yield dan return di generator?

yield menjeda generator dan menyerahkan sebuah nilai, tetapi fungsinya bisa dilanjutkan dari titik terakhir pada panggilan next() berikutnya. return mengakhiri generator untuk selamanya: ia mengatur done: true dan tidak ada nilai lagi yang keluar. Kamu bisa yield berkali-kali; return hanya bermakna sekali.

Kapan sebaiknya memakai generator daripada array?

Saat urutannya tanpa akhir, mahal untuk dihitung, atau kamu hanya butuh sebagian nilainya. Generator menghasilkan item satu per satu sesuai permintaan, jadi kamu bisa mewakili aliran ID tanpa akhir atau hasil API yang berhalaman tanpa harus membuat semuanya di awal. Kalau kamu sudah punya array kecil yang tetap, pakai saja array-nya.

Ilustrasi bahasa pemrograman di Coddy

Belajar coding dengan Coddy

Mulai