Cara Baru Iterasi di ES6 dengan Iterable dan Iterator

  • ES6 mengenalkan cara baru iterasi: for-of.
  • for-of melakukan iterasi terhadap objek yang iterable.
  • Iterable protocol mensyaratkan objek untuk mengimplemen metode iterator yang mengembalikan objek yang memenuhi protokol iterator pada prototipe [Symbol.iterator]-nya untuk dapat dianggap iterable.
  • Iterator protocol mensyaratkan sebuah objek untuk mengembalikan fungsi next yang kemudian mengembalikan objek {value, done} untuk dapat dianggap sebagai iterator.

Layaknya jalan ke Roma, Javascript juga memiliki beberapa jalan untuk melakukan iterasi. Beberapa yang umum kita temui antara lain for loop, while loop, dan do-while loop. Mari kita lihat contoh kasus iterasi terhadap sebuah array di bawah.

const arr = [0, 1, 2, 3, 4, 5];

// for loop klasik
for (let i = 0; i < arr.length; i++) {
  console.log(arr[i]);
}

// while loop klasik
const j = 0;
while (j < arr.length) {
  console.log(arr[j]);
  j++;
}

// do-while, agak jarang ditemukan
const k = 0;
do {
  console.log(arr[k]);
  k++;
} while (k < arr.length);

Ketiga cara klasik beriterasi tersebut seringkali sudah cukup untuk program kita. Tapi, ES6 membawa cara baru beriterasi yang cukup menarik: for-of.

For-of Loop

Iterasi dengan for-of memiliki sintaks yang sangat ringkas dan bersih. Masih dengan contoh array arr yang sama:

for (item of arr) {
  console.log(item);
}

Ternyata, selain array, for-of loop ini bisa digunakan juga untuk iterasi terhadap objek-objek lain.

const str = "Hello";

for (char of str) {
  console.log(char);
} // "H", "e", "l", "l", "o"

const s = new Set(str);

for (val of s) {
  console.log(val);;
} // "H", "e", "l", "o"

const m = new Map([ [1, 2], [3, 4] ]);

for (val of m) {
  console.log(val);
} // [1, 2], [3, 4]

Selain array, ternyata sintaks iterasi yang menarik ini juga bisa diimplementasi terhadap objek String, Set dan Map. Kenapa begitu? Mari kita tengok apa kata MDN tentang for-of:

"The for...of statement creates a loop Iterating over iterable objects" - Mozilla Developer Network

Ternyata terdapat klasifikasi objek bernama "iterable", di mana Array, String, Set dan Map tergabung di dalamnya. Jika Anda berasal dari bahasa pemrograman Java, mungkin Anda secara intuisi akan menebak bahwa iterable merujuk kepada suatu interface. Namun, Javascript tidak mengenal interface, yang dikenal Javascript adalah protokol, dan iterable ini adalah salah satu di antaranya.

Protokol Iterable

Protokol di Javascript adalah konvensi yang harus dipenuhi suatu objek untuk dapat memanfaatkan fitur tertentu dari bahasa pemrograman Javascript. Konsep protokol diperkenalkan pada ES6 dengan protokol iterable dan iterator. Seperti yang sudah dipaparkan di atas, objek yang memenuhi protokol iterable dapat menggunakan kekuatan for-of.

Untuk dapat dianggap iterable, sebuah objek (atau objek lain di rantai prototipenya) harus menerapkan metode @@iterator yaitu sebuah fungsi yang mengembalikan objek iterator. Fungsi tersebut harus disematkan pada properti Symbol.iterator objek tersebut. Simbol tersebut adalah salah satu simbol built-in dari Javascript, spesifik untuk menunjuk ke metode @@iterator suatu objek.

Kalau begitu, Array, String, Set dan Map semua punya metode iterator ya? Betul sekali!

const a = [];
typeof a[Symbol.iterator]; // "function"

const s = new Set();
typeof s[Symbol.iterator]; // "function"

const m = new Map();
typeof m[Symbol.iterator]; // "function"

Kita bisa mengimplementasi objek iterator kita sendiri dengan mengisi properti [Symbol.iterator] dengan metode iterator.

const obj = { name: 'Faisal', age: 17 };

function myIterator() { // implementasi metode iterator kita }

obj[Symbol.iterator] = myIterator;

Kita juga bisa loh meminjam metode iterator dari objek lain (dan memang faedah dari protokol ini salah satunya agar iterator dapat dipakai ulang).

const obj = {
  0: 'a',
  1: 'b',
  2: 'c',
  3: 'd',
  length: 4,

  // hehehe
  [Symbol.iterator]: Array.prototype[Symbol.iterator],
};

for (value of obj) console.log(value); // "a", "b", "c", "d"

Fungsi yang kita sematkan sebagai metode iterator ke objek kita tidak bisa sembarang fungsi, ia harus memenuhi protokol lain bernama protokol iterator.

Protokol Iterator

Protokol berikutnya yang akan kita eksplorasi adalah protokol iterator. Protokol iterator mengklasifikasi objek yang bisa menentukan perilaku iterasi suatu objek. Agar suatu objek dapat menjadi iterator, protokol ini mensyaratkan objek tersebut untuk mengimplementasi sebuah fungsi next yang:

  • Tidak menerima argumen.
  • Mengembalikan sebuah objek dengan properti:
  • value yang mengembalikan nilai apapun yang valid dalam Javascript.
  • done yang mengembalikan nilai Boolean yang mengisyaratkan apakah iterator dapat menghasilkan value selanjutnya atau tidak. Objek kembalian tanpa properti done akan dianggap sama dengan mengembalikan {value, done: false} yang berarti iterasi masih dapat dilanjutkan. Sementara itu, jika done berisi true, iterator tidak akan mengembalikan value.

Talk is confusing, show me the code:

// fungsi ini adalah metode iterator
function myIterator() {
  let n = 0;

  // fungsi ini mengembalikan objek iterator
  return {
    next: function() {
      n++;
      return { value: n, done: false };
    },
  };
}

const it = myIterator(); // it adalah sebuah objek iterator

it.next(); // { value: 1, done: false }
it.next(); // { value: 2, done: false }
it.next(); // { value: 3, done: false }

Perhatikan bahwa iterator tersebut tidak akan pernah menghasilkan nilai done yang true, artinya iterasi dengan iterator ini akan menjadi tidak terbatas alias infinite loop. Ya, itu dibolehkan.

Sekarang mari kita coba iterator dengan perilaku lain dalam sebuah objek, katakanlah kita ingin iterator kita membuat genap semua angka ganjil di dalam array yang kita punya dengan mengalikannya dengan 2, sementara angka yang sudah genap dibiarkan.

const arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

function evenizingIterator() {
  let idx = 0;

  return {
    // menggunakan arrow function agar `this` terikat
    // ke konteks array yang disematkan
    next: () => {
      if (idx < this.length) {
        const value = this[idx] % 2 === 0 ? this[idx] : this[idx] * 2;
        idx = idx + 1;
        return { value: value, done: false };
      } else {
        return { done: true };
      }
    },
  };
}

arr[Symbol.iterator] = evenizingIterator;
for (v of arr) console.log(v);
// 0, 2, 2, 6, 4, 10, 6, 14, 8, 18, 10

Lalu, apakah menjadikan sebuah objek iterable hanya berguna untuk menggunakan for-loop? Tentu saja tidak, terdapat banyak lagi sintaks Javascript yang mengonsumsi iterable.

// Array.from
const arr2 = Array.from(arr);

// spread, rest dan destructuring assignment
const arr3 = [...arr];

const [a, b, c] = [...arr];

const [d, e, f, ...g] = [...arr];

// contoh-contoh lain
Promise.all(iterable);

Promise.race(iterable);

Lalu, Apa Keuntungan Menggunakan Iterable?

Yang pertama, seperti yang kita lihat di atas, iterable membuat kita dapat menggunakan beberapa fitur native yang dimiliki Javascript. Selain itu:

  • Memisahkan logika konsumsi data dari logika bisnis. Dengan memisahkan iterator, ekspresi iterasi yang perlu kita tulis bebas dari logika dan lebih mudah dibaca.
  • Memudahkan penggunaan ulang logika konsumsi data, ingat bahwa iterator dapat dipakai ulang.
  • Mengonsumsi data dari sumber data dengan panjang yang tidak diketahui, atau bahkan tidak terbatas lewat metode iterator.next().

Sekian ulasan tentang iterable dan iterator, semoga dapat membantu iterasi Anda ke depannya. Sebenarnya ada satu lagi bahasan terkait, yaitu generator yang merupakan factory dari iterator, tapi karena sudah terlalu panjang akan saya pisahkan menjadi tulisan tersendiri. Tunggu tanggal mainnya ya!

Referensi lanjutan: