Kita sudah menelusuri array dengan for...of sejak section Arrays, dan dua chapter terakhir memperlihatkan Map serta Set ikut ditelusuri dengan pola yang sama. Object biasa, sebaliknya, ditolak. Pertanyaannya: apa yang menyatukan array, string, Map, dan Set — dan memisahkan mereka dari object? Jawabannya bukan kebetulan, melainkan sepasang kontrak bawaan bahasa: protokol iterable dan protokol iterator.

Memahami kedua protokol ini memberi dua keuntungan. Pertama, kita tahu persis apa yang dilakukan for...of, spread, dan Array.from saat menerima sebuah nilai. Kedua, kita bisa membuat struktur data sendiri — deret hitung mundur, antrean khusus — yang berperilaku sama rapi dengan struktur bawaan saat ditelusuri.

Protokol Iterable #

Aturan protokol: sebuah nilai disebut iterable bila memiliki method bernama Symbol.iterator yang mengembalikan sebuah iterator. Symbol.iterator adalah built-in symbol dari section Values dan Types — dan karena nama propertinya berasal dari ekspresi, penulisannya memakai computed property [Symbol.iterator] seperti di section Objects. Bila nilai apa pun dimintai method ini dan hasilnya bukan function, nilai itu bukan iterable:

js
const harga = { kopi: 12000 };
const antrean = ["kopi", "teh"];
console.log(typeof harga[Symbol.iterator]);
console.log(typeof antrean[Symbol.iterator]);

Output yang Dihasilkan:

text
undefined
function

Object biasa menghasilkan undefined — tidak ada method Symbol.iterator di dalamnya — sedangkan array menghasilkan function. Inilah pembeda teknis yang menjelaskan mengapa for...of diterima array dan ditolak object. Kelompok nilai bawaan yang iterable: array, string, Map, dan Set.

Method yang sama itu dipakai oleh seluruh konsumen iterasi bawaan bahasa. for...of memakainya di dalam loop, spread ... menghabiskan isinya menjadi elemen array atau argumen function, Array.from mengumpulkannya menjadi array, dan destructuring array membongkarnya ke beberapa variabel. Satu method Symbol.iterator yang benar membuat sebuah object langsung didukung keempat konsumen tersebut — tanpa satu penyesuaian pun di sisi konsumen. String pun termasuk kelompok ini: penelusurannya menghasilkan karakter satu per satu, sehingga perulangan per karakter tidak membutuhkan index sama sekali.

Protokol Iterator: next(), value, dan done #

Aturan protokol kedua: iterator adalah object dengan method next(). Setiap pemanggilan next() mengembalikan object hasil berbentuk { value, done } — value berisi elemen berikutnya, dan done bernilai true ketika elemen sudah habis. for-of hanyalah otomatisasi dari tiga langkah ini: minta iterator lewat Symbol.iterator(), panggil next() berulang kali, berhenti saat done bernilai true.

Satu sifat iterator penting untuk diingat: iterator adalah object sekali pakai. Kondisi posisi saat ini — sudah di elemen ke berapa — tersimpan di dalam iterator itu sendiri, bukan di sumber datanya. Menelusuri array yang sama dua kali berarti meminta dua iterator berbeda; iterator lama yang sudah selesai tidak bisa dimundurkan ke awal. Karena itu penelusuran berulang selalu dimulai dari panggilan [Symbol.iterator]() yang baru.

Berkas berikut menjalankan ketiga langkah itu dengan tangan pada array, lalu mendefinisikan iterable sendiri bernama hitungMundur untuk hitungan mundur pembukaan promo:

// hitung-mundur.js
// Protokol iterator: penelusuran manual, lalu custom iterable buatan sendiri.

const antrean = ["Siti", "Budi", "Rina"];
const penelusur = antrean[Symbol.iterator]();
console.log(penelusur.next());
console.log(penelusur.next());
console.log(penelusur.next());
console.log(penelusur.next());

// Custom iterable: hitung mundur pembukaan promo
const hitungMundur = {
  mulai: 3,
  [Symbol.iterator]() {
    let sisa = this.mulai;
    return {
      next() {
        if (sisa <= 0) {
          return { done: true, value: undefined };
        }
        return { done: false, value: sisa-- };
      },
    };
  },
};

for (const detik of hitungMundur) {
  console.log(`Promo dibuka dalam ${detik}...`);
}
console.log([...hitungMundur]);

Output yang Dihasilkan:

text
{ value: 'Siti', done: false }
{ value: 'Budi', done: false }
{ value: 'Rina', done: false }
{ value: undefined, done: true }
Promo dibuka dalam 3...
Promo dibuka dalam 2...
Promo dibuka dalam 1...
[ 3, 2, 1 ]

Mari kita telaah baris-baris pentingnya:

  1. Baris 5 (antrean[Symbol.iterator]()): array dimintai iterator bawaannya — inilah panggilan yang dilakukan for...of secara diam-diam di awal penelusuran.
  2. Baris 6–8 (penelusur.next() tiga kali): setiap pemanggilan mengembalikan satu elemen dalam bungkusan { value, done: false } — Siti, lalu Budi, lalu Rina.
  3. Baris 9 (penelusur.next() keempat): setelah elemen habis, hasilnya { value: undefined, done: true }; iterator yang sudah selesai akan terus menjawab done: true bila dipanggil lagi.
  4. Baris 13 ([Symbol.iterator]() {): object hitungMundur mendefinisikan method dengan nama computed dari Symbol.iterator — sejak baris ini hitungMundur sah sebagai iterable.
  5. Baris 15 (let sisa = this.mulai;): setiap pemanggilan method ini membuat variabel sisa sendiri, sehingga setiap penelusuran baru selalu memulai dari 3, bukan melanjutkan penelusuran sebelumnya.
  6. Baris 16–23 (return { next() { ... } }): iterator dikembalikan sebagai object literal dengan satu method next; variabel sisa tetap hidup antar pemanggilan next() karena terikat closure — materi section Functions.
  7. Baris 19 dan 21 (dua bentuk hasil): bila sisa sudah 0, iterator mengembalikan { done: true, value: undefined }; selain itu { done: false, value: sisa-- } — sisa-- mengembalikan nilai dulu, baru menurunkannya.
  8. Baris 27–29 (for...of atas hitungMundur): for-of memanggil [Symbol.iterator]() sekali, lalu next() berulang sampai done — kita tidak pernah menyentuh iteratornya secara langsung.
  9. Baris 30 ([...hitungMundur]): spread adalah konsumen iterable juga; deret 3, 2, 1 dikumpulkan menjadi array baru.

Kapan Custom Iterable Berguna #

Custom iterable cocok untuk deret yang bisa dihitung bertahap tanpa menyimpan seluruh elemennya di memori: hitungan mundur, nomor antrean, atau langkah berulang dengan aturan tertentu. Contoh kedua ini menomori tiket antrean layanan dari angka satu sampai batas kursi. Konsumennya seragam — for...of, spread, dan Array.from semua berlaku tanpa penyesuaian:

js
const antreanLayanan = {
  totalKursi: 2,
  [Symbol.iterator]() {
    let nomor = 1;
    const batas = this.totalKursi + 1;
    return {
      next() {
        if (nomor >= batas) {
          return { done: true, value: undefined };
        }
        return { done: false, value: `Nomor ${nomor++}` };
      },
    };
  },
};
console.log(Array.from(antreanLayanan));
for (const tiket of antreanLayanan) {
  console.log(tiket);
}

Output yang Dihasilkan:

text
[ 'Nomor 1', 'Nomor 2' ]
Nomor 1
Nomor 2

Pola desainnya sama dengan hitungMundur: variabel nomor dan batas disiapkan di dalam [Symbol.iterator](), lalu method next() menghitung nilai berikutnya sampai batas tercapai. Array.from menghabiskan iterator menjadi array lengkap, sedangkan for...of menikmati elemen satu per satu — keduanya memanggil protokol yang sama.

Pemilihan bentuk konsumen mengikuti kebutuhan. for...of cocok saat elemen dipakai begitu dihasilkan — mencetak, menghitung, berhenti dini dengan break. Array.from atau spread cocok saat seluruh deret dibutuhkan sekaligus sebagai array, misalnya untuk dilanjutkan ke map atau filter. Deret yang sangat panjang justru paling aman lewat for...of, karena elemennya dihitung satu per satu dan tidak pernah tersimpan utuh di memori.

Dua kesalahan paling sering saat menulis custom iterable pertama: mengembalikan object hasil { value, done } langsung dari [Symbol.iterator]() — padahal yang harus dikembalikan adalah iterator, yaitu object yang punya method next() — dan membaca this di dalam next() sambil mengira itu menunjuk object iterable. next() dijalankan pada object iterator, bukan pada object induknya; karena itu kondisi seperti sisa dan batas disalin ke variabel lokal di dalam [Symbol.iterator]() sebelum iterator dibentuk. Kedua kesalahan itu tercegah oleh pola yang sama: siapkan keadaan di method iterable, kembalikan iterator yang hanya mengurus langkah.

Dengan protokol ini terbaca, chapter berikutnya menutup pembahasan dari sisi konsumen: sintaks for...of itu sendiri, method entries(), keys(), dan values(), serta bedanya dengan for...in.

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

hitung-mundur.js

Unduh berkas
javascript
// hitung-mundur.js
// Protokol iterator: penelusuran manual, lalu custom iterable buatan sendiri.

const antrean = ["Siti", "Budi", "Rina"];
const penelusur = antrean[Symbol.iterator]();
console.log(penelusur.next());
console.log(penelusur.next());
console.log(penelusur.next());
console.log(penelusur.next());

// Custom iterable: hitung mundur pembukaan promo
const hitungMundur = {
  mulai: 3,
  [Symbol.iterator]() {
    let sisa = this.mulai;
    return {
      next() {
        if (sisa <= 0) {
          return { done: true, value: undefined };
        }
        return { done: false, value: sisa-- };
      },
    };
  },
};

for (const detik of hitungMundur) {
  console.log(`Promo dibuka dalam ${detik}...`);
}
console.log([...hitungMundur]);