Українська

Повний посібник з розуміння та реалізації протоколу ітератора JavaScript, що дозволить вам створювати власні ітератори для покращеної обробки даних.

Розкриваємо протокол ітератора JavaScript та власні ітератори

Протокол ітератора в JavaScript надає стандартизований спосіб обходу структур даних. Розуміння цього протоколу дозволяє розробникам ефективно працювати з вбудованими ітерованими об'єктами, такими як масиви та рядки, а також створювати власні ітеровані об'єкти, пристосовані до конкретних структур даних та вимог додатку. Цей посібник пропонує всебічне дослідження протоколу ітератора та способів реалізації власних ітераторів.

Що таке протокол ітератора?

Протокол ітератора визначає, як об'єкт може бути ітерований, тобто як можна послідовно отримати доступ до його елементів. Він складається з двох частин: протоколу Iterable (ітерований об'єкт) та протоколу Iterator (ітератор).

Протокол Iterable (ітерований об'єкт)

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

По суті, ітерований об'єкт знає, як створити для себе ітератор.

Протокол Iterator (ітератор)

Протокол Iterator визначає, як отримувати значення з послідовності. Об'єкт вважається ітератором, якщо він має метод next(), який повертає об'єкт з двома властивостями:

Метод next() є робочою конячкою протоколу ітератора. Кожен виклик next() просуває ітератор і повертає наступне значення в послідовності. Коли всі значення повернуті, next() повертає об'єкт з `done`, встановленим у `true`.

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

JavaScript надає кілька вбудованих структур даних, які є ітерованими за своєю природою. До них належать:

Ці ітеровані об'єкти можна безпосередньо використовувати з циклом for...of, синтаксисом розповсюдження (...) та іншими конструкціями, що покладаються на протокол ітератора.

Приклад з масивами:


const myArray = ["apple", "banana", "cherry"];

for (const item of myArray) {
  console.log(item); // Вивід: apple, banana, cherry
}

Приклад з рядками:


const myString = "Hello";

for (const char of myString) {
  console.log(char); // Вивід: H, e, l, l, o
}

Цикл for...of

Цикл for...of є потужною конструкцією для ітерації по ітерованих об'єктах. Він автоматично обробляє складнощі протоколу ітератора, що полегшує доступ до значень у послідовності.

Синтаксис циклу for...of:


for (const element of iterable) {
  // Код, що виконується для кожного елемента
}

Цикл for...of отримує ітератор з ітерованого об'єкта (використовуючи Symbol.iterator) і багаторазово викликає метод next() ітератора, доки `done` не стане `true`. На кожній ітерації змінній `element` присвоюється значення властивості `value`, повернуте методом `next()`.

Створення власних ітераторів

Хоча JavaScript надає вбудовані ітеровані об'єкти, справжня сила протоколу ітератора полягає в можливості визначати власні ітератори для ваших структур даних. Це дозволяє вам контролювати, як дані обходяться та як до них отримується доступ.

Ось як створити власний ітератор:

  1. Визначте клас або об'єкт, що представляє вашу власну структуру даних.
  2. Реалізуйте метод Symbol.iterator у вашому класі або об'єкті. Цей метод повинен повертати об'єкт ітератора.
  3. Об'єкт ітератора повинен мати метод next(), який повертає об'єкт з властивостями value та done.

Приклад: Створення ітератора для простого діапазону

Створимо клас під назвою Range, який представляє діапазон чисел. Ми реалізуємо протокол ітератора, щоб дозволити ітерацію по числах у цьому діапазоні.


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let currentValue = this.start;
    const that = this; // Зберігаємо 'this' для використання всередині об'єкта ітератора

    return {
      next() {
        if (currentValue <= that.end) {
          return {
            value: currentValue++,
            done: false,
          };
        } else {
          return {
            value: undefined,
            done: true,
          };
        }
      },
    };
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Вивід: 1, 2, 3, 4, 5
}

Пояснення:

Приклад: Створення ітератора для зв'язного списку

Розглянемо ще один приклад: створення ітератора для структури даних зв'язного списку. Зв'язний список — це послідовність вузлів, де кожен вузол містить значення та посилання (вказівник) на наступний вузол у списку. Останній вузол у списку має посилання на null (або undefined).


class LinkedListNode {
    constructor(value, next = null) {
        this.value = value;
        this.next = next;
    }
}

class LinkedList {
    constructor() {
        this.head = null;
    }

    append(value) {
        const newNode = new LinkedListNode(value);
        if (!this.head) {
            this.head = newNode;
            return;
        }

        let current = this.head;
        while (current.next) {
            current = current.next;
        }
        current.next = newNode;
    }

    [Symbol.iterator]() {
        let current = this.head;

        return {
            next() {
                if (current) {
                    const value = current.value;
                    current = current.next;
                    return {
                        value: value,
                        done: false
                    };
                } else {
                    return {
                        value: undefined,
                        done: true
                    };
                }
            }
        };
    }
}

// Приклад використання:
const myList = new LinkedList();
myList.append("London");
myList.append("Paris");
myList.append("Tokyo");

for (const city of myList) {
    console.log(city); // Вивід: London, Paris, Tokyo
}

Пояснення:

Генераторні функції

Генераторні функції надають більш стислий та елегантний спосіб створення ітераторів. Вони використовують ключове слово yield для генерації значень за вимогою.

Генераторна функція визначається за допомогою синтаксису function*.

Приклад: Створення ітератора за допомогою генераторної функції

Перепишемо ітератор Range, використовуючи генераторну функцію:


class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  *[Symbol.iterator]() {
    for (let i = this.start; i <= this.end; i++) {
      yield i;
    }
  }
}

const myRange = new Range(1, 5);

for (const number of myRange) {
  console.log(number); // Вивід: 1, 2, 3, 4, 5
}

Пояснення:

Генераторні функції спрощують створення ітераторів, автоматично обробляючи метод next() та прапорець done.

Приклад: Генератор послідовності Фібоначчі

Ще один чудовий приклад використання генераторних функцій — генерація послідовності Фібоначчі:


function* fibonacciSequence() {
  let a = 0;
  let b = 1;

  while (true) {
    yield a;
    [a, b] = [b, a + b]; // Деструктуруюче присвоєння для одночасного оновлення
  }
}

const fibonacci = fibonacciSequence();

for (let i = 0; i < 10; i++) {
  console.log(fibonacci.next().value); // Вивід: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
}

Пояснення:

Переваги використання протоколу ітератора

Просунуті техніки роботи з ітераторами

Комбінування ітераторів

Ви можете комбінувати кілька ітераторів в один. Це корисно, коли вам потрібно обробити дані з кількох джерел у єдиний спосіб.


function* combineIterators(...iterables) {
  for (const iterable of iterables) {
    for (const item of iterable) {
      yield item;
    }
  }
}

const array1 = [1, 2, 3];
const array2 = ["a", "b", "c"];
const string1 = "XYZ";

const combined = combineIterators(array1, array2, string1);

for (const value of combined) {
  console.log(value); // Вивід: 1, 2, 3, a, b, c, X, Y, Z
}

У цьому прикладі функція `combineIterators` приймає будь-яку кількість ітерованих об'єктів як аргументи. Вона ітерує по кожному ітерованому об'єкту та повертає кожен елемент за допомогою `yield`. Результатом є єдиний ітератор, який генерує всі значення з усіх вхідних ітерованих об'єктів.

Фільтрація та трансформація ітераторів

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


function* filterIterator(iterable, predicate) {
  for (const item of iterable) {
    if (predicate(item)) {
      yield item;
    }
  }
}

function* mapIterator(iterable, transform) {
  for (const item of iterable) {
    yield transform(item);
    }
}

const numbers = [1, 2, 3, 4, 5, 6];

const evenNumbers = filterIterator(numbers, (x) => x % 2 === 0);
const squaredEvenNumbers = mapIterator(evenNumbers, (x) => x * x);

for (const value of squaredEvenNumbers) {
    console.log(value); // Вивід: 4, 16, 36
}

Тут `filterIterator` приймає ітерований об'єкт та функцію-предикат. Він повертає за допомогою `yield` лише ті елементи, для яких предикат повертає `true`. `mapIterator` приймає ітерований об'єкт та функцію-трансформатор. Він повертає за допомогою `yield` результат застосування функції-трансформатора до кожного елемента.

Застосування в реальному світі

Протокол ітератора широко використовується в бібліотеках та фреймворках JavaScript, і він є цінним у різноманітних реальних додатках, особливо при роботі з великими наборами даних або асинхронними операціями.

Найкращі практики

Висновок

Протокол ітератора JavaScript надає потужний та гнучкий спосіб обходу структур даних. Розуміючи протоколи Iterable та Iterator, а також використовуючи генераторні функції, ви можете створювати власні ітератори, пристосовані до ваших конкретних потреб. Це дозволяє ефективно працювати з даними, покращувати читабельність коду та підвищувати продуктивність ваших додатків. Оволодіння ітераторами відкриває глибше розуміння можливостей JavaScript та дає змогу писати більш елегантний та ефективний код.