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♣'