Ітератори та генератори

Обробка кожного елемента колекції є дуже розповсюдженою операцією. JavaScript надає численні способи перебору колекції, від простих циклів for до map() та filter(). Ітератори та генератори додають концепцію перебору безпосередньо у базову мову та надають механізм налаштування поведінки циклів for...of.

Більш детально дивіться у статтях:

Ітератори

У JavaScript ітератор - це об'єкт, який визначає послідовність та, за бажанням, значення, що повертається по її завершенні. Якщо конкретніше, то ітератор - це будь-який об'єкт, що реалізує протокол ітератора, маючи метод next(), який повертає об'єкт з двома властивостями: value, наступне значення послідовності; та done, що дорівнює true, якщо останнє значення послідовності вже було отримане. Якщо поруч з done присутнє значення value, воно є поверненим значенням ітератора.

Як тільки ітератор був створений, його можна явно обходити, викликаючи метод next(). Перебір ітератора називають споживанням ітератора, бо, загалом, це можна зробити лише один раз. Після того, як завершувальне значення було видане, додаткові виклики next() мають просто вертати {done: true}.

Найтиповішим ітератором у Javascript є ітератор масиву, який просто повертає по черзі кожне значення відповідного масиву. Хоча легко уявити, що усі ітератори можна виразити у вигляді масивів, це не так. Масиви мають бути розташовані у пам'яті цілком, але ітератори споживаються лише за необхідності і, таким чином, можуть виражати послідовності необмеженого розміру, такі як діапазон цілих чисел між 0 та Infinity (нескінченністю).

Ось приклад, який може робити саме це. Він дозволяє створювати ітератор простого діапазону, який визначає послідовність цілих чисел від start (включно) до end (не включно) з проміжком step. Його кінцеве повернене значення є розміром створеної послідовності, що відслідковується змінною iterationCount.

function makeRangeIterator(start = 0, end = Infinity, step = 1) {
    let nextIndex = start;
    let iterationCount = 0;

    const rangeIterator = {
       next: function() {
           let result;
           if (nextIndex < end) {
               result = { value: nextIndex, done: false }
               nextIndex += step;
               iterationCount++;
               return result;
           }
           return { value: iterationCount, done: true }
       }
    };
    return rangeIterator;
}

Далі використання ітератора виглядає наступним чином:

let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

console.log("Перебрано послідовність розміром: ", result.value); // [повертає 5 чисел з інтервалу від 0 до 10]

Неможливо знати, чи певний об'єкт є ітератором. Якщо вам необхідно це зробити, використовуйте ітерабельні об'єкти.

Функції-генератори

Хоча користувацькі ітератори є потужним інструментом, їхнє створення вимагає обережного програмування через необхідність явно підтримувати їхній внутрішній стан. Функції-генератори надають потужну альтернативу: вони дозволяють визначати алгоритм перебору написанням єдиної функції, виконання якої не є безперервним. Функції-генератори пишуться за допомогою синтаксису function*. При першому виклику функції-генератори не виконують свій код, замість цього вони повертають ітератор під назвою Generator. Коли значення споживається викликом методу об'єкта Generator next, функція-генератор виконується до тих пір, поки не зустріне ключове слово yield.

Функція може викликатись будь-яку бажану кількість раз та кожен раз вертатиме новий об'єкт Generator, однак, кожний об'єкт Generator може перебиратись лише один раз.

Тепер ми можемо адаптувати наведений вище приклад. Поведінка коду ідентична, але ця реалізація набагато простіша у написанні та читанні.

function* makeRangeIterator(start = 0, end = 100, step = 1) {
    let iterationCount = 0;
    for (let i = start; i < end; i += step) {
        iterationCount++;
        yield i;
    }
    return iterationCount;
}

Ітерабельні об'єкти

Об'єкт є ітерабельним, якщо він визначає свою ітераційну поведінку, наприклад, через які значення проходитиме цикл for...of. Деякі вбудовані типи, такі як Array або Map, мають визначену за замовчуванням ітераційну поведінку, в той час, як інші типи (такі як Object) її не мають.

Для того, щоб бути ітерабельним, об'єкт повинен реалізувати метод @@iterator, тобто, цей об'єкт (або один з об'єктів у його ланцюжку прототипів) повинен мати властивість з ключем Symbol.iterator.

Може бути можливо перебрати ітерабельний об'єкт більше одного разу, або лише один раз. Який варіант обирати, вирішує програміст. Ітерабельні об'єкти, які можна перебрати лише один раз (наприклад, об'єкти Generator) традиційно повертають this зі свого методу @@iterator, тоді як ті, які можна перебирати багаторазово, повинні повертати новий ітератор на кожний виклик @@iterator.

function* makeIterator() {
    yield 1;
    yield 2;
}

const it = makeIterator();

for(const itItem of it) {
    console.log(itItem);
}

console.log(it[Symbol.iterator]() === it) // true;

// Це приклад показує, що генератор(ітератор) є ітерабельним об'єктом, метод якого @@iterator повертає сам об'єкт (it),
// і з цієї причини цей об'єкт (it) можна перебирати лише один раз


// Якщо ми змінимо метод @@iterator об'єкта "it" на функцію/генератор, що вертає новий ітератор/генератор, цей об'єкт (it)
// можна буде перебирати багато разів

it[Symbol.iterator] = function* () {
  yield 2;
  yield 1;
};

Створені користувачем ітерабельні об'єкти

Ми можемо створювати власні ітерабельні об'єкти наступним чином:

var myIterable = {
    *[Symbol.iterator]() {
        yield 1;
        yield 2;
        yield 3;
    }
}

for (let value of myIterable) { 
    console.log(value); 
}
// 1
// 2
// 3

або

[...myIterable]; // [1, 2, 3]

Вбудовані ітерабельні об'єкти

String, Array, TypedArray, Map та Set усі є вбудованими ітерабельними об'єктами, тому що всі їхні прототипи мають метод Symbol.iterator.

Синтаксис, що очікує на ітерабельний об'єкт

Деякі оператори та вирази очікують на ітерабельні об'єкти, наприклад, цикли for-of, yield*.

for (let value of ['а', 'б', 'в']) {
    console.log(value);
}
// "а"
// "б"
// "в"

[...'абв']; // ["а", "б", "в"]

function* gen() {
  yield* ['а', 'б', 'в'];
}

gen().next(); // { value: "а", done: false }

[a, b, c] = new Set(['а', 'б', 'в']);
a; // "а"

Просунуті генератори

Генератори обчислюють значення, які видають, на вимогу, це дозволяє їм ефективно відображати послідовності, затратні для обчислення, чи навіть нескінченні послідовності, як було продемонстровано вище.

Метод next() також приймає значення, які можуть використовуватись для модифікації внутрішнього стану генератора. Значення, передане у метод next() буде отримане оператором yield. Зауважте, що значення, передане у перший метод next(), завжди ігнорується.

Ось генератор Фібоначчі, який використовує next(x), щоб перезапустити послідовність:

function* fibonacci() {
  var fn1 = 0;
  var fn2 = 1;
  while (true) {  
    var current = fn1;
    fn1 = fn2;
    fn2 = current + fn1;
    var reset = yield current;
    if (reset) {
        fn1 = 0;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
console.log(sequence.next().value);     // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
console.log(sequence.next().value);     // 5
console.log(sequence.next().value);     // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2

Ви можете змусити генератор викинути виняток, викликавши його метод throw() та передавши значення винятку, яке він має викинути. Цей виняток буде викинутий з поточного призупиненого контексту генератора, так, якби поточний оператор зупинки yield був оператором throw значення.

Якщо виняток не перехоплений всередині генератора, він поширюватиметься до виклику throw(), і наступні виклики методу next() отримають властивість done, що дорівнює true.

Генератори мають метод return(value), який повертає надане значення та завершує сам генератор.