JavaScript · Синтаксис · Просунутий рівень
Ітератори та генератори
Створюйте власні ітерабельні об'єкти через `Symbol.iterator`, пишіть функції-генератори з `function*` та `yield`, використовуйте `yield*` для делегування.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
yield*, передача значень через .next(), async генератори
#Таблиця Iterator/Iterable/Generator, споживачі, нескінченні послідовності
#Вправи:
Реалізувати протокол ітератора
#Створіть об'єкт `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
}
Функція-генератор з 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
}
Нескінченний генератор: Фібоначчі
#Напишіть нескінченний генератор `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
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]
Зробити клас ітерабельним
#Створіть клас `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);
Конвеєр генераторів
#Побудуйте конвеєр з трьох генераторів: `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]
Значення, що повертає генератор
#Напишіть генератор `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 + ' ');
}
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
Асинхронний генератор з 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();
Додати 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♣'