Perulakan for klasik dan method forEach bekerja baik untuk array, tetapi keduanya mengandaikan data sudah utuh di memori. Dunia nyata sering tidak seberuntung itu: baris berkas besar yang harus diproses satu per satu, halaman hasil API yang tiba bertahap, deret matematis yang secara alami tak hingga. JavaScript menjawab kebutuhan tersebut dengan dua bangunan yang saling melengkapi: protokol iterator — kontrak bagaimana sebuah object menyerahkan nilainya satu per satu — dan generator — cara menulis iterator tanpa menulis mesin statusnya secara manual. Pada chapter penutup Section ini kita akan membedah keduanya, lalu melihat mengapa “malas” bisa berarti efisien.

for…of dan Iterable #

Statement for...of adalah konsumen tertinggi protokol iterator: memberinya apa pun yang iterable, dan statement ini tahu cara memintai nilainya satu per satu. Array hanyalah salah satu warga dunia iterable — string, Map, Set, hingga NodeList semuanya memenuhi kontrak yang sama. Berkas for-of.mjs memutar empat jenis iterable sekaligus:

js
const bahasa = ["JavaScript", "Python", "Go"];
for (const nama of bahasa) {
  console.log(`bahasa   : ${nama}`);
}

const huruf = [];
for (const karakter of "Node") {
  huruf.push(karakter);
}
console.log(`string   : ${huruf.join("-")}`);

const warna = new Map([
  ["primer", "merah"],
  ["sekunder", "hijau"],
]);
for (const [kelas, nilai] of warna) {
  console.log(`map      : ${kelas} = ${nilai}`);
}

const tag = new Set(["backend", "cli", "backend"]);
for (const t of tag) {
  console.log(`set      : ${t}`);
}

Output yang Dihasilkan:

text
bahasa   : JavaScript
bahasa   : Python
bahasa   : Go
string   : N-o-d-e
map      : primer = merah
map      : sekunder = hijau
set      : backend
set      : cli

Empat pengamatan dari hasil di atas:

  1. Array: setiap elemen bahasa hadir satu per satu — tanpa indeks, tanpa length, tanpa risiko meleset satu posisi.
  2. String: for...of berjalan per code point, bukan per unit UTF-16, sehingga karakter hasil tetap utuh meski berisi emoji pada kasus lain.
  3. Map: setiap langkah menyerahkan pasangan [kunci, nilai] yang langsung dibongkar destructuring — persis pola dari Chapter Object dan Destructuring.
  4. Set: nilai "backend" yang ditulis dua kali hanya muncul sekali, sesuai sifat Set sebagai kumpulan nilai unik dengan urutan penyisipan.

Protokol Iterator: Symbol.iterator #

Di balik setiap for...of berlangsung negosiasi baku. Sebuah object disebut iterable bila object tersebut memiliki method dengan kunci Symbol.iterator — symbol bawaan yang diketahui JavaScript engine. Method itu dipanggil sekali di awal perulangan dan harus mengembalikan sebuah iterator: object dengan method next(). Setiap panggilan next() mengembalikan { value, done } — done: false menyertakan nilai, done: true menandakan perulangan selesai. Kita bisa memenuhi kontrak ini dengan class buatan sendiri, seperti Rentang pada iterator-kustom.mjs:

js
class Rentang {
  constructor(awal, akhir) {
    this.awal = awal;
    this.akhir = akhir;
  }

  [Symbol.iterator]() {
    let sekarang = this.awal;
    const akhir = this.akhir;
    return {
      next() {
        if (sekarang <= akhir) {
          return { value: sekarang++, done: false };
        }
        return { done: true };
      },
    };
  }
}

const rentang = new Rentang(1, 4);
const hasil = [];
for (const n of rentang) {
  hasil.push(n);
}
console.log(`for...of       : ${hasil.join(", ")}`);

// Protokol mentah: memanggil next() satu per satu
const iterator = rentang[Symbol.iterator]();
console.log(`next() pertama : ${JSON.stringify(iterator.next())}`);
console.log(`next() kedua   : ${JSON.stringify(iterator.next())}`);
console.log(`lompat dua     : ${JSON.stringify(rentang[Symbol.iterator]().next())}`);

Output yang Dihasilkan:

text
for...of       : 1, 2, 3, 4
next() pertama : {"value":1,"done":false}
next() kedua   : {"value":2,"done":false}
lompat dua     : {"value":1,"done":false}

Mari kita bedah implementasinya:

  1. [Symbol.iterator]() sebagai nama method: tanda kurung siku memungkinkan kunci berupa ekspresi — dalam hal ini symbol Symbol.iterator — teknik yang sama dengan computed property pada object biasa.
  2. Method next() menutup state di dalam closure: variable sekarang hidup hanya di iterator tersebut, sehingga Rentang instance sendiri tetap bersih dan bisa diputar berkali-kali.
  3. Baris for...of membuktikan kontrak bekerja dua arah: Rentang yang sama kini bisa dipakai statement perulangan standar tanpa method tambahan apa pun.
  4. Tiga baris terakhir memperlihatkan protokol mentahnya: memanggil next() dua kali menghasilkan 1 lalu 2, dan memanggil [Symbol.iterator]() kembali mencetak iterator baru yang kembali mulai dari 1 — setiap panggilan menghasilkan kursor independen.

Generator: function* dan yield #

Menulis next() beserta state-nya secara manual cepat atau lambat merepotkan. Generator menyelesaikannya: function yang dideklarasikan dengan function* berhenti di setiap yield dan mengembalikan nilai, lalu melanjutkan persis dari titik berhenti tersebut saat next() dipanggil lagi. Object generator yang dihasilkan sudah memenuhi protokol iterator secara otomatis. Berkas generator.mjs merangkum fitur intinya:

js
function* hitungMundur(dari) {
  while (dari > 0) {
    yield dari;
    dari--;
  }
}

function* gabungkan() {
  yield* hitungMundur(3);
  yield* ["selesai"];
}

const langkah = [];
for (const n of gabungkan()) {
  langkah.push(n);
}
console.log(`gabungan : ${langkah.join(", ")}`);

// Lazy: nilai dihitung hanya saat diminta
const mundur = hitungMundur(5);
console.log(`ambil 2  : ${mundur.next().value}, ${mundur.next().value}`);
console.log(`sisa     : ${[...mundur].join(", ")}`);

Output yang Dihasilkan:

text
gabungan : 3, 2, 1, selesai
ambil 2  : 5, 4
sisa     : 3, 2, 1

Penjelasan alurnya:

  1. yield dari: setiap yield adalah titik jeda yang menyerahkan satu nilai ke pemanggil next(). Loop while di dalam hitungMundur menghasilkan 3, 2, 1 tanpa array perantara.
  2. yield* mendelegasikan ke iterable lain: gabungkan() menyerahkan seluruh hasil hitungMundur(3), lalu seluruh elemen array ["selesai"] — keduanya menyatu menjadi 3, 2, 1, selesai.
  3. Bagian “lazy” membuktikan sifat paling berharga generator: dua nilai pertama diambil dengan next(), dan yang mengejutkan, [...mundur] setelahnya hanya menghasilkan 3, 2, 1 — lima nilai awal sudah tidak dihitung ulang. Generator mengingat posisinya; nilai keenam sampai terakhir baru akan dihitung jika ada yang memintanya.

Deret Tak Hingga dan Lazy Evaluation #

Loop while (true) biasanya adalah bencana, tetapi pada generator deret tak hingga justru pola yang sah: karena nilai hanya dihitung saat diminta, konsumen yang menentukan kapan berhenti lewat break. Kasus klasiknya adalah deret Fibonacci, seperti pada lazy.mjs:

js
function* fibonacci() {
  let a = 0;
  let b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const bawah50 = [];
for (const n of fibonacci()) {
  if (n >= 50) {
    break; // deret tak hingga, kita berhenti dengan break
  }
  bawah50.push(n);
}
console.log(`fibonacci < 50 : ${bawah50.join(", ")}`);

Output yang Dihasilkan:

text
fibonacci < 50 : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

Generator fibonacci tidak tahu dan tidak peduli seberapa banyak nilai yang dibutuhkan pemanggilnya — function tersebut hanya menjanjikan: “setiap kali diminta, kuberikan suku berikutnya”. Loop konsumen mengambil nilai selama masih di bawah 50, lalu break menghentikan permintaan. Tidak ada array raksasa yang dibangun lebih dulu, dan jika kebutuhan berubah menjadi “suku pertama yang melebihi satu juta”, generator yang sama dipakai tanpa perubahan satu baris pun.

Dunia Node.js memakai pola ini di banyak tempat: membaca berkas besar baris demi baris, stream yang menyediakan potongan data saat tersedia, hingga paginasi hasil API — semuanya adalah “nilai tiba bertahap” yang bentuk paling murninya kita lihat pada bab ini. Konsep ini akan kembali muncul secara besar-besaran pada Section Buffers dan Streams.

Referensi lanjutan untuk topik chapter ini:

Dengan iterator dan generator, perjalanan kita melalui fitur bahasa JavaScript yang paling penting untuk Node.js tuntas: variabel dan scope, tipe data, function, object, array, class, error handling, promise, hingga iterasi malas. Perbendaharaan itu akan kita bawa menyusun kode menjadi aplikasi yang nyata — dimulai dari cara memecah program ke berkas-berkas yang saling terhubung. Selamat datang di Section Modules dan Packages.