Колекція – це структура даних, котра містить елементи. Наприклад, рядок – це набір символів, а масив – набір впорядкованих елементів:

const message = 'ok'; // містить символи 'o' та 'k'

const numbers = [1, 5, 10]; // містить числа 1, 5, 10

Для доступу до елементів колекцій з різною структурою даних, в JavaScript реалізовано спеціальний шаблон (pattern) ітератор, котрий дає змогу обходити елементи колекцій.

Розглянемо приклад, коли є масив, і потрібно вивести у консоль кожний елемент цього масиву:

const numbers = [1, 5, 10];

for (const item of numbers) {
  console.log(item);
}

// 1, 5, 10

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

Тепер, в якості експерименту, спробуємо за допомогою for...of перебрати властивості звичайного JavaScript-об’єкта (докладніше про object):

const person = {
  name: 'Virgil',
  age: 25,
};

for (const prop of person) {
  console.log(prop);
}

Результатом виконання прикладу вище стане помилка – TypeError: person is not iterable, котра натякає, що цикл for...of вимагає наявності iterable об’єкту, для перебору його елементів. Тепер слід розібратись, що таке iterable об’єкт.

Інтерфейси Iterable та Iterator

Об’єкт є iterable (тобто його можна перебирати), якщо він відповідає інтерфейсу Iterable, котрий вимагає наявність в об’єкта методу Symbol.iterator, який повинен повертати об’єкт iterator:

interface Iterable {
    [Symbol.iterator]() {
      //...
      return Iterator;
    }
}

Об’єкт iterator повинен відповідати інтерфейсу Iterator, котрий вимагає наявність в об’єкта методу next(). Метод next повинен повертати об’єкт з властивостями: done (логічне значення, що вказує на кінець ітерації) і value (елемент, отриманий з колекції на поточній ітерації):

interface Iterator {
    next() {
       //...
       return {
          value: <value>,
          done: <boolean>
       };
    };
}

Iterable об’єкт

В якості прикладу розглянемо масив (array), та визначимо яким чином він відповідає інтерфейсу Iterable. Розглянемо приклад:

const numbers = [1, 5, 10];
console.log(numbers[Symbol.iterator]());

// Object [Array Iterator] {}

Виклик виразу numbers[Symbol.iterator]() демонструє, що екземпляр масиву містить спеціальний метод Symbol.iterator, чим підтверджує відповідність масиву інтерфейсу Iterable.

numbers[Symbol.iterator]() повертає об’єкт Iterator, котрий дає змогу виконувати ітерацію над елементами масиву. Для доступу до кожного елемента масиву, достатньо викликати iterator.next():

const numbers = [1, 5, 10];
iterator = numbers[Symbol.iterator]();

console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 10, done: false }

Кожний виклик iterator.next() повертає об’єкт: { value: <item>, done: <boolean> }. Властивість value містить елемент ітерації, властивість done вказує чи завершено ітерацію.

Якщо елементів для ітерації більше немає, iterator.next() повертає { value: undefined, done: true }.

Споживачі iterable об’єктів

JavaScript надає набір інструментів для роботи з об’єктами iterable (цикли, синтаксичні конструкції, функції).

Цикл for…of

Цикл for...of приймає iterable об’єкт, та перебирає його елементи:

const text = 'ok';

for (const char of text) {
  console.log(char);
}

// o, k

Spread оператор

Ще одним споживачем iterable об’єктів, є spread оператор [...iterable]:

const text = 'hello';
const chars = [...text];

console.log(chars);

// [ 'h', 'e', 'l', 'l', 'o' ]

В прикладі вище, spread оператор [...text] перебирає символи рядка, і створює масив з цих символів.

Деструктор масиву

Синтаксис деструкції масивів, також може робити деструкцію iterable об’єктів:

const text = 'hello';

const [firstChar, ...restChars] = text;

console.log(firstChar); // h
console.log(restChars); // [ 'e', 'l', 'l', 'o' ]

const [firstChar, ...restChars] = text – присвоєння, де змінній firstChar присвоюється перший символ рядка ‘h’, а решта символів text зберігаються у масив restChars.

Array.from()

Array.from(iterable[, mapFunction]) приймає iterable об’єкт, і повертає масив з його елементів:

const text = 'hello';
const chars = Array.from(text);

console.log(chars);

// [ 'h', 'e', 'l', 'l', 'o' ]

Iterable типи у JavaScript

Розглянемо деякі популярні iterable типи даних у JavaScript, окрім вже розглянутих array та string.

Map

Об’єкт Map є iterable, та дає змогу перебирати пари з ключів та значень:

const person = new Map();
person.set('name', 'Marcus');
person.set('age', 28);

for (const [key, value] of person) {
  console.log(`${key}: ${value}`);
}

// name: Marcus
// age: 28

Set

Об’єкт Set є iterable, та дає змогу перебирати його елементи:

const letters = new Set(['a', 'b', 'c']);

for (const item of letters) {
  console.log(item);
}

// a, b, c

Підсумок

Iterable – це об’єкт колекція, котрий можна перебирати. Щоб бути iterable, об’єкт повинен відповідати інтерфейсу Iterable.

JavaScript має вбудований набір споживачів для роботи з iterable об’єктами, наприклад – цикл for...of, spread оператор, деструктор масивів, Array.from(). Корисність шаблону iterator полягає в тому, що будь-який iterable об’єкт, може бути використаний будь-яким споживачем iterable об’єктів.

Також є можливість визначати власні iterable типи та споживачі iterable об’єктів, що буде розглянуто в окремому записі.