JavaScript · Синтаксис · Просунутий рівень

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

10 завдань

Створюйте власні ітерабельні об'єкти через `Symbol.iterator`, пишіть функції-генератори з `function*` та `yield`, використовуйте `yield*` для делегування.

Протокол ітератора, ліниві обчислення та функції-генератори

#
**Навіщо існують ітератори** Масиви нетерплячі — вони зберігають всі значення в пам'яті одразу. Ітератори ліниві — вони виробляють одне значення за раз, на вимогу. Це важливо для великих наборів даних, нескінченних послідовностей або дорогих обчислень де потрібна лише частина значень: ```js // Масив: всі 1 000 000 чисел одразу в пам'яті const arr = Array.from({ length: 1_000_000 }, (_, i) => i); // Генератор: одне число на виклик .next(), майже не використовує пам'ять function* range(n) { for (let i = 0; i < n; i++) yield i; } for (const x of range(1_000_000)) { /* ... */ } ``` **Протокол ітератора** Об'єкт є *iterable* якщо має метод `[Symbol.iterator]()` що повертає *iterator*. Ітератор — об'єкт з методом `next()` що повертає `{ value, done }`: ```js const counter = { [Symbol.iterator]() { let n = 1; return { next() { return n <= 3 ? { value: n++, done: false } : { value: undefined, done: true }; } }; } }; for (const x of counter) console.log(x); // 1 2 3 console.log([...counter]); // [1, 2, 3] ``` Вбудовані iterables: `Array`, `String`, `Map`, `Set`, `arguments`, `NodeList`. **Функції-генератори — простіший спосіб** Писати ітератори вручну повторювально. Функції-генератори (`function*`) дозволяють записати ту саму логіку з набагато меншим кодом. Кожен `yield` призупиняє виконання і відправляє значення назовні: ```js function* counter(start, end) { for (let i = start; i <= end; i++) { yield i; // пауза тут, відправити i назовні } } for (const n of counter(1, 3)) console.log(n); // 1 2 3 console.log([...counter(1, 5)]); // [1, 2, 3, 4, 5] const g = counter(1, 2); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: undefined, done: true } ```

yield*, передача значень через .next(), async генератори

#
**`yield*` — делегування до іншого iterable** ```js function* abc() { yield 'a'; yield 'b'; yield 'c'; } function* nums() { yield 1; yield 2; } function* combined() { yield* abc(); // делегує: виробляє 'a', 'b', 'c' yield* nums(); // потім 1, 2 yield* [3, 4]; // працює з будь-яким iterable } console.log([...combined()]); // ['a','b','c',1,2,3,4] ``` **Метод-генератор класу — робимо об'єкти iterable** ```js class Tree { constructor(value, children = []) { this.value = value; this.children = children; } *[Symbol.iterator]() { yield this.value; for (const child of this.children) { yield* child; // рекурсивне делегування } } } const tree = new Tree(1, [new Tree(2, [new Tree(4)]), new Tree(3)]); console.log([...tree]); // [1, 2, 4, 3] ``` **Передача значень до генератора через `.next(value)`** Значення передане в `.next(value)` стає *результатом* виразу `yield` всередині генератора — двостороннє спілкування: ```js function* accumulator() { let total = 0; while (true) { const n = yield total; // відправляє total; отримує наступний .next(n) if (n === null) return total; total += n; } } const acc = accumulator(); acc.next(); // { value: 0, done: false } acc.next(10); // { value: 10, done: false } acc.next(20); // { value: 30, done: false } acc.next(null);// { value: 30, done: true } ``` **Async генератори — пагінація, потоки** ```js async function* paginate(url) { let page = 1; while (true) { const res = await fetch(`${url}?page=${page++}`); const json = await res.json(); if (!json.items.length) break; yield* json.items; } } for await (const user of paginate('/api/users')) { console.log(user.name); } ```

Таблиця Iterator/Iterable/Generator, споживачі, нескінченні послідовності

#
**Iterator vs Iterable vs Generator** | Концепція | Має | Використовується з | |---|---|---| | Iterable | `[Symbol.iterator]()` | `for...of`, spread, деструктуризація | | Iterator | `.next()` → `{value, done}` | Прямі виклики `.next()` | | Generator | обидва (є Iterator + Iterable) | обидва | **Місця де iterables споживаються** ```js for (const x of iter) {} // for...of const arr = [...iter]; // spread const [a, b, ...rest] = iter; // деструктуризація Array.from(iter) // Array.from new Set(iter) / new Map(iter) // конструктори Set/Map Promise.all(iter) // Promise-комбінатори ``` **Нескінченні генератори** ```js function* naturals(start = 1) { while (true) yield start++; } function* take(n, iterable) { let count = 0; for (const x of iterable) { if (count++ >= n) return; yield x; } } console.log([...take(5, naturals())]); // [1, 2, 3, 4, 5] ``` **Раннє завершення: `return()` та `finally`** ```js function* withCleanup() { try { yield 1; yield 2; yield 3; } finally { console.log('cleanup!'); } } const g = withCleanup(); g.next(); // { value: 1, done: false } g.return('end'); // 'cleanup!' -> { value: 'end', done: true } for (const x of withCleanup()) { if (x === 1) break; // 'cleanup!' теж друкується } ``` **Генератор vs Масив** | Ситуація | Використовувати | |---|---| | Всі значення потрібні одразу | Масив | | Великі/нескінченні дані | Генератор | | Async джерело (API, потоки) | Async генератор | | Потрібні `.map`/`.filter`/`.reduce` | Масив |
01

Реалізувати протокол ітератора

#

Створіть об'єкт `Counter`, що реалізує протокол ітератора вручну. Він має рахувати від `start` до `end` (включно). Додайте метод `[Symbol.iterator]()`, що повертає об'єкт з методом `next()`. Використайте у циклі `for...of`.

function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      // поверніть об'єкт ітератора з next()
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
Рішення
function makeCounter(start, end) {
  return {
    [Symbol.iterator]() {
      let current = start;
      return {
        next() {
          if (current <= end) {
            return { value: current++, done: false };
          }
          return { value: undefined, done: true };
        }
      };
    }
  };
}

for (const n of makeCounter(1, 5)) {
  console.log(n);  // 1 2 3 4 5
}
02

Функція-генератор з function*

#

Напишіть функцію-генератор `range(start, end, step = 1)`, що видає числа від `start` до `end` (не включаючи), з кроком `step`. Використайте для виводу чисел 0, 2, 4, 6, 8.

function* range(start, end, step = 1) {
  // видавайте числа тут
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
Рішення
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  console.log(n);  // 0 2 4 6 8
}
03

Нескінченний генератор: Фібоначчі

#

Напишіть нескінченний генератор `fibonacci()`, що видає послідовність Фібоначчі нескінченно (0, 1, 1, 2, 3, 5, 8, ...). Використайте його для виводу перших 8 чисел Фібоначчі через `.next()` або виходу з циклу `for...of` з лічильником.

function* fibonacci() {
  // нескінченна послідовність
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
Рішення
function* fibonacci() {
  let [a, b] = [0, 1];
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}

const gen = fibonacci();
for (let i = 0; i < 8; i++) {
  console.log(gen.next().value);
}
// 0 1 1 2 3 5 8 13
04

yield* — делегування іншому генератору

#

Напишіть два генератори: `odds(n)` видає непарні числа 1, 3, 5, ..., до n; `evens(n)` видає парні числа 2, 4, 6, ..., до n. Напишіть третій генератор `oddsAndEvens(n)`, що використовує `yield*` спочатку для делегування `odds(n)`, потім `evens(n)`.

function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  // використовуйте yield* тут
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
Рішення
function* odds(n) {
  for (let i = 1; i <= n; i += 2) yield i;
}

function* evens(n) {
  for (let i = 2; i <= n; i += 2) yield i;
}

function* oddsAndEvens(n) {
  yield* odds(n);
  yield* evens(n);
}

console.log([...oddsAndEvens(6)]);
// [1, 3, 5, 2, 4, 6]
05

Зробити клас ітерабельним

#

Створіть клас `NumberRange` з `constructor(start, end)`. Додайте `[Symbol.iterator]()` як метод-генератор, щоб екземпляри працювали з `for...of`, spread та деструктуризацією.

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

  *[Symbol.iterator]() {
    // видавайте значення тут
  }
}

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);
Рішення
class NumberRange {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

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

const r = new NumberRange(1, 5);
console.log([...r]);        // [1, 2, 3, 4, 5]
const [a, b] = r;
console.log(a, b);          // 1 2
for (const n of r) console.log(n);
06

Конвеєр генераторів

#

Побудуйте конвеєр з трьох генераторів: `naturals()` — нескінченний: 1, 2, 3, ...; `take(gen, n)` — перші n значень з генератора; `squares(gen)` — відображає кожне значення в його квадрат. Виведіть перші 5 квадратів через `take(squares(naturals()), 5)`.

function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  // видайте перші n значень з gen
}

function* squares(gen) {
  // видайте x*x для кожного x з gen
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
Рішення
function* naturals() {
  let n = 1;
  while (true) yield n++;
}

function* take(gen, n) {
  let count = 0;
  for (const val of gen) {
    if (count++ >= n) break;
    yield val;
  }
}

function* squares(gen) {
  for (const x of gen) yield x * x;
}

console.log([...take(squares(naturals()), 5)]);
// [1, 4, 9, 16, 25]
07

Значення, що повертає генератор

#

Напишіть генератор `countdown(n)`, що видає n, n-1, ..., 1, а потім повертає `'Done!'`. Покажіть, що значення return з'являється в останньому виклику `.next()` як `{ value: 'Done!', done: true }`, але НЕ видається `for...of`.

function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 'Done!', done: true }

for (const n of countdown(3)) {
  process.stdout.write(n + ' ');
}
Рішення
function* countdown(n) {
  while (n > 0) yield n--;
  return 'Done!';
}

const gen = countdown(3);
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next()); // { value: 'Done!', done: true }

for (const n of countdown(3)) {
  process.stdout.write(n + ' ');
}
08

Spread та деструктуризація з власними ітерабельними

#

Створіть клас `LinkedList`, де кожен вузол має `value` та `next`. Додайте `[Symbol.iterator]()`, щоб список можна було розгорнути в масив та деструктурувати. Побудуйте список 1 → 2 → 3 та перевірте, що `[...list]` дає `[1, 2, 3]`.

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

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    // обхід від head
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
Рішення
class LinkedList {
  constructor() { this.head = null; }

  push(value) {
    this.head = { value, next: this.head };
    return this;
  }

  *[Symbol.iterator]() {
    let node = this.head;
    while (node) {
      yield node.value;
      node = node.next;
    }
  }
}

const list = new LinkedList();
list.push(3).push(2).push(1);
console.log([...list]);         // [1, 2, 3]
const [first, second] = list;
console.log(first, second);     // 1 2
09

Асинхронний генератор з for await...of

#

Напишіть асинхронний генератор `asyncRange(start, end, delay)`, що видає числа від `start` до `end`, чекаючи `delay` мс між кожним. Споживайте його через `for await...of` всередині асинхронної функції `main()`.

async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  // використовуйте for await...of тут
}

main();
Рішення
async function* asyncRange(start, end, delay) {
  for (let i = start; i <= end; i++) {
    await new Promise(r => setTimeout(r, delay));
    yield i;
  }
}

async function main() {
  for await (const n of asyncRange(1, 4, 50)) {
    console.log(n);  // 1 2 3 4
  }
}

main();
10

Додати Symbol.iterator до звичайного об'єкта

#

Маючи `const deck = { suits: ['♠','♥','♦','♣'], values: ['A','2','3'] }`, додайте метод-генератор `[Symbol.iterator]`, що видає всі комбінації карт (`'A♠'`, `'A♥'`, ..., `'3♣'`). Перевірте через `[...deck]`.

const deck = {
  suits: ['♠', '♥', '♦', '♣'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    // видайте всі комбінації значень та масть
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A♠'
console.log([...deck][11]);       // '3♣'
Рішення
const deck = {
  suits: ['♠', '♥', '♦', '♣'],
  values: ['A', '2', '3'],
  *[Symbol.iterator]() {
    for (const value of this.values) {
      for (const suit of this.suits) {
        yield `${value}${suit}`;
      }
    }
  }
};

console.log([...deck].length);   // 12
console.log([...deck][0]);        // 'A♠'
console.log([...deck][11]);       // '3♣'