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:
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:
bahasa : JavaScript
bahasa : Python
bahasa : Go
string : N-o-d-e
map : primer = merah
map : sekunder = hijau
set : backend
set : cliEmpat pengamatan dari hasil di atas:
- Array: setiap elemen
bahasahadir satu per satu — tanpa indeks, tanpalength, tanpa risiko meleset satu posisi. - String:
for...ofberjalan per code point, bukan per unit UTF-16, sehingga karakter hasil tetap utuh meski berisi emoji pada kasus lain. Map: setiap langkah menyerahkan pasangan[kunci, nilai]yang langsung dibongkar destructuring — persis pola dari Chapter Object dan Destructuring.Set: nilai"backend"yang ditulis dua kali hanya muncul sekali, sesuai sifatSetsebagai 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:
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:
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:
[Symbol.iterator]()sebagai nama method: tanda kurung siku memungkinkan kunci berupa ekspresi — dalam hal ini symbolSymbol.iterator— teknik yang sama dengan computed property pada object biasa.- Method
next()menutup state di dalam closure: variablesekaranghidup hanya di iterator tersebut, sehinggaRentanginstance sendiri tetap bersih dan bisa diputar berkali-kali. - Baris
for...ofmembuktikan kontrak bekerja dua arah:Rentangyang sama kini bisa dipakai statement perulangan standar tanpa method tambahan apa pun. - Tiga baris terakhir memperlihatkan protokol mentahnya: memanggil
next()dua kali menghasilkan1lalu2, dan memanggil[Symbol.iterator]()kembali mencetak iterator baru yang kembali mulai dari1— 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:
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:
gabungan : 3, 2, 1, selesai
ambil 2 : 5, 4
sisa : 3, 2, 1Penjelasan alurnya:
yield dari: setiapyieldadalah titik jeda yang menyerahkan satu nilai ke pemanggilnext(). Loopwhiledi dalamhitungMundurmenghasilkan3, 2, 1tanpa array perantara.yield*mendelegasikan ke iterable lain:gabungkan()menyerahkan seluruh hasilhitungMundur(3), lalu seluruh elemen array["selesai"]— keduanya menyatu menjadi3, 2, 1, selesai.- Bagian “lazy” membuktikan sifat paling berharga generator: dua nilai pertama diambil dengan
next(), dan yang mengejutkan,[...mundur]setelahnya hanya menghasilkan3, 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:
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:
fibonacci < 50 : 0, 1, 1, 2, 3, 5, 8, 13, 21, 34Generator 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.