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

Багато мов програмування відмовились від ітерування по даних з допомогою циклу for, який потребує ініціалізації змінної для відслідковування позиції у колекції, на користь об’єктів–ітераторів, котрі програмно повертають наступний елемент у колекції. Ітератори роблять роботу з колекціями даних легшою, а ECMAScript 6 додає ітератори у JavaScript. Разом з новими методами масивів та новими типами колекцій (як от множини та мапи), ітератори є ключем до ефективної обробки даних, і саме тому ви знайдете їх у багатьох частинах мови. Новий цикл for-of працює з ітераторами, оператор розкладу (...) використовує ітератори, ітератори навіть роблять асинхронне програмування простішим.

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

Проблема з циклами

Якщо ви до цього програмували на JavaScript, ви напевно писали колись такий код:

var colors = ["red", "green", "blue"];

for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}

Тут стандартний цикл for відслідковує індекс у масиві colors через змінну i. Значення i збільшується щоразу, коли виконується ітерація, якщо значення i є меншим за довжину масиву (що зберігається у len).

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

Що таке ітератори?

Ітератори є простими об’єктами з певним інтерфейсом для ітерування. Всі об’єкти–ітератори мають метод next(), що повертає об’єкт–результат. Об’єкт–результат має дві властивості: value, яка є наступним значенням та done, яка є булевим значенням, котре рівне true, якщо більше не залишилось значень, які потрібно повернути. Ітератор зберігає внутрішній вказівник на розташування у колекції, і з кожним викликом методу next() він повертає потрібне значення.

Якщо ви викличете next() після останнього значення, що було повернуте, метод поверне done рівне true та value, що містить повернене значення (return value) для ітератора. Це повернене значення не є частиною множини даних, але є останньою частинкою цих даних або undefined, якщо даних немає. Повернене значення в ітераторів дуже схоже на повернене значення у функцій у тому, що це остання можливість передати інформацію у місце, звідки їх викликали.

Знаючи це, створити генератор з використанням ECMAScript 5 дуже просто:

function createIterator(items) {

    var i = 0;

    return {
        next: function() {

            var done = (i >= items.length);
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };

        }
    };
}

var iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// для всіх наступних викликів
console.log(iterator.next());           // "{ value: undefined, done: true }"

Функція createIterator() повертає об’єкт з методом next(). Щоразу, коли метод викликається, в якості value повертається наступне значення з масиву items. Коли i дорівнює 3, done стає true, а тернарний оператор, що встановлює value, обчислюється в undefined. Ці два результати відповідають за останній випадок для ітераторів у ECMAScript 6, коли next() викликається для ітератора після того, як було використано останню частину даних.

Як показує приклад, написання ітераторів, що поводяться відповідно до правил, які покладені в ECMAScript 6, є дещо складним.

На щастя, ECMAScript 6 також надає нам генератори, які роблять створення об’єктів–ітераторів набагато простішим.

Що таке генератори?

Генератор — це функція, що повертає ітератор. Функція–генератори позначаються символом зірочки (*) після ключового слова function та використовує ключове слово yield. Неважливо, чи зірочка стоїть відразу після function, чи між ним та символом * є проміжки, ось приклад:

// генератор
function *createIterator() {
    yield 1;
    yield 2;
    yield 3;
}

// генератори викликаються як звичайні функції, але повертають ітератор
let iterator = createIterator();

console.log(iterator.next().value);     // 1
console.log(iterator.next().value);     // 2
console.log(iterator.next().value);     // 3

Символ * перед createIterator() робить цю функцію генератором. Ключове слово yield є також новим у ECMAScript 6 і визначає значення, які кінцевому ітератору слід буде повертати при виклику next() у тому порядку, в якому вони мають повертатись. Ітератор, що був згенерований у цьому прикладі, має три різних значення, що повертаються при послідовних викликах методу next(): перше 1, друге 2 та третє 3. Генератор викликається як і будь–яка інша функція, що і показано при створенні iterator.

Можливо найцікавішим аспектом функцій–генераторів є те, що вони зупиняють виконання після кожного оператора yield. Наприклад, після виконання yield 1 у цьому коді, функція не виконує нічого іншого, доки не буде викликано метод next(). Лише тоді виконається yield 2. Така можливість зупинки виконання всередині функції є надзвичайно потужною і має декілька цікавих застосувань у функціях–генераторах (про це піде мова у розділі «Розширена функціональність ітераторів»).

Ключове слово yield можна використовувати з будь–яким значення або виразом, тому ви можете написати функцію–генератор, що додає елементи у ітератор без відслідковування елементів один за одним. Наприклад, ось один зі способів використання yield всередині циклу for:

function *createIterator(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
}

let iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// для всіх наступних викликів
console.log(iterator.next());           // "{ value: undefined, done: true }"

Цей приклад у функцію–генератор createIterator() передається масив items. Всередині функції, цикл for віддає елементи з масиву в ітератор по мірі виконання циклу. Щоразу, коли зустрічається yield, цикл зупиняється, і щоразу, коли для iterator викликається next(), цикл продовжується до наступного оператора yield.

Функції–генератори є важливим нововведенням ECMAScript 6, і оскільки це всього лише функції, вони можуть використовуватись у тому ж місці, де і функції. Решта цього розділу фокусується на інших способах написання генераторів.

W> Ключове слово yield може використовуватись лише всередині генераторів. Використання yield будь–де, включаючи функції всередині генераторів, призведе до синтаксичної помилки:

function *createIterator(items) {

    items.forEach(function(item) {

        // синтаксична помилка
        yield item + 1;
    });
}

W> Навіть хоча й yield технічно в середині createIterator(), цей код матиме синтаксичну помилку тому, що yield не може перетинати функціональні межі. У цьому випадку, yield схожий на return у тому, що вкладена функція не може повернути значення з функції, у якій вона міститься.

Вираз функції–генератора

Ви можете використовувати функціональний вираз для створення генераторів просто додавши символ зірочки (*) між ключовим слово function та відкриваючою круглою дужкою. Наприклад:

let createIterator = function *(items) {
    for (let i = 0; i < items.length; i++) {
        yield items[i];
    }
};

let iterator = createIterator([1, 2, 3]);

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

// для всіх наступних викликів
console.log(iterator.next());           // "{ value: undefined, done: true }"

У цьому коді, createIterator() є виразом функції–генератора замість оголошення функції. Зірочка розташована між ключовим словом function та відкриваючою круглою дужкою, тому що функціональний вираз є анонімним. Все інше у цьому прикладі є таким же, як і в попередній версії функції createIterator(), яка використовує цикл for.

I> Створити arrow–функцію яка буде генератором - неможливо.

Методи–генератори в об’єктах

Оскільки генератори є простими функціями, їх також можна додавати в об’єкти. Наприклад, ви можете зробити генератор у об’єктному літералі в стилі ECMAScript 5 через функціональний вираз:

var o = {

    createIterator: function *(items) {
        for (let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);

Ви можете також використати лаконічні методи з ECMAScript 6, додавши перед ім’ям методу зірочку (*):

var o = {

    *createIterator(items) {
        for (let i = 0; i < items.length; i++) {
            yield items[i];
        }
    }
};

let iterator = o.createIterator([1, 2, 3]);

Ці приклади функціонально еквіваленті до прикладу у розділі «Вираз функції–генератора» - вони просто використовують різний синтаксис. У прикладі з лаконічним методом, оскільки метод createIterator() заданий без ключового слова function, зірочка розташована відразу перед ім’ям методу, але ви можете залишити пробіл між ними.

Ітерабельні об’єкти та for-of

Дуже близькими до ітераторів є ітерабельні об’єкти (iterable) — це об’єкти з властивістю Symbol.iterator. Добревідомий символ Symbol.iterator задає функцію, що повертає ітератор для даного об’єкту. Всі об’єкти колекцій (масиви, множини та мапи) та рядки є ітерательними в ECMAScript 6, тобто вони мають визначений за замовчуванням ітератор. Ітерабельні об’єкти спроектовані для використання з новим доповненням у ECMAScript: циклом for-of.

I> Всі ітератори, які створені з допомогою генераторів, також є ітерабельними, бо генератори за замовчуванням присвоюють властивість Symbol.iterator.

На початку цієї глави я згадав про проблему відслідковування індексу всередині циклу for. Ітератори є першою частиною вирішення цієї проблеми. Цикл for-of є іншою частиною: з ним не потрібно слідкувати за індексом всередині колекції, і це дозволяє вам сконцентруватись на роботі з її вмістом.

Цикл for-of викликає next() ітерабельного об’єкта під час кожного виконання циклу та зберігає value з отриманого об’єкта у змінну. Цикл продовжує виконання допоки властивість done у об’єкта, що повертається не буде дорівнювати true. Ось приклад:

let values = [1, 2, 3];

for (let num of values) {
    console.log(num);
}

Цей код виводить наступне:

1
2
3

Цей цикл for-of спершу викликає метод Symbol.iterator для масиву values, щоб отримати ітератор (Виклик Symbol.iterator відбувається за кулісами всередині самого рушія JavaScript.) Тоді викликається iterator.next(), а властивість value об’єкта, який повертається з ітератора, читається в num. Змінна num приймає значення 1, потім 2 і нарешті 3. Коли done в об’єкті, що повертається, буде true, цикл закінчує виконання, тому num ніколи не присвоюється значення undefined.

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

W> Оператор for-of кине помилку при спробі використати його з неітерабельним об’єктом, null або undefined.

Доступ до ітератора за замовчуванням

Ви можете скористатись Symbol.iterator для доступу до ітератора об’єкта за замовчуванням, ось так:

let values = [1, 2, 3];
let iterator = values[Symbol.iterator]();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

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

Оскільки Symbol.iterator задає ітератор за замовчуванням, ви можете використовувати його, щоб визначати, чи об’єкт є ітерабельним:

function isIterable(object) {
    return typeof object[Symbol.iterator] === "function";
}

console.log(isIterable([1, 2, 3]));     // true
console.log(isIterable("Hello"));       // true
console.log(isIterable(new Map()));     // true
console.log(isIterable(new Set()));     // true
console.log(isIterable(new WeakMap())); // false
console.log(isIterable(new WeakSet())); // false

Функцію isIterable() просто перевіряє, чи ітератор за замовчуванням існує і є функцією. Цикл for-of робить таку ж перевірку перед виконанням.

Раніше у цьому розділі було показано способи використання Symbol.iterator з вбудованими ітерабельними типами, проте ви також можете використовувати властивість Symbol.iterator для створення власних ітерабельних об’єктів.

Створення ітерабельних об’єктів

Об’єкти, що були задані розробниками, не є ітерабельними за замовчуванням, але ви можете зробити їх такими через створення властивості Symbol.iterator, що містить генератор. Наприклад:

let collection = {
    items: [],
    *[Symbol.iterator]() {
        for (let item of this.items) {
            yield item;
        }
    }

};

collection.items.push(1);
collection.items.push(2);
collection.items.push(3);

for (let x of collection) {
    console.log(x);
}

Цей код виводить наступне:

1
2
3

Спочатку приклад визначає ітератор за замовчуванням для об’єкта collection. Ітератор за замовчуванням створюється через метод Symbol.iterator, який є генератором (зверніть увагу, що перед ім’ям стоїть зірочка). Генератор використовує цикл for-of для ітерації значеннями та yield - для повернення кожного. Замість простого ітерування для визначення значень, які ітератор за замовчуванням для collection має повернути, об’єкт collection покладається на ітератор за замовчуванням в this.items, що виконає цю роботу.

I> Далі у цій главі розділ «Делегування генераторів» описує різні підходи використання ітераторів інших об’єктів.

Тепер ви побачили декілька способів використання ітератора за замовчуванням для масивів, але є набагато більше вбудованих у ECMAScript 6 ітераторів, щоб зробити роботу з колекціями даних простішою.

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

Ітератори є важливою частиною ECMAScript 6, і, як слід здогадатись, немає потреби створювати власні ітератори для більшості вбудованих типів: мова включає їх за замовчуванням. Вам може знадобитись створення власних ітераторів лише тоді, коли вбудовані ітератори не відповідатимуть вашим потребам, що найчастіше стається тоді, коли ви визначаєте власні об’єкти або класи. В іншому випадку, у виконанні своєї роботи ви можете покластись на вбудовані ітератори. Можливо, найбільш поширеними є ітератори для роботи з колекціями.

Ітератори колекцій

ECMAScript 6 має три типи об’єктів–колекцій: масиви, мапи та множини. Для роботи з їхнім вмістом всі три мають такі вбудовані ітератори:

  • entries() - повертає ітератор, значеннями якого будуть пари “ключ–значення”;
  • values() - повертає ітератор, значення якого будуть значеннями колекції;
  • keys() - повертає ітератор, значення якого будуть ключами, що містяться у колекції;

Ви можете отримати ітератор для колекції через виклик одного з цих трьох методів.

Ітератор entries()

Ітератор entries() при кожному виклику next() повертає двохелементний масив. Двоелементний масив відображає ключ та значення кожного елементу колекції. Для масивів першим елементом буде числовий індекс; для множин першим елементом також буде значення (оскільки у множинах значення дублюють ключі); для мап перший елемент буде ключем.

Ось кілька прикладів використання цього ітератора:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let entry of colors.entries()) {
    console.log(entry);
}

for (let entry of tracking.entries()) {
    console.log(entry);
}

for (let entry of data.entries()) {
    console.log(entry);
}

Виклики console.log() дають такий вивід:

[0, "red"]
[1, "green"]
[2, "blue"]
[1234, 1234]
[5678, 5678]
[9012, 9012]
["title", "Understanding ECMAScript 6"]
["format", "ebook"]

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

Ітератор values()

Ітератор values() просто повертає значення так, як вони були збережені у колекцію. Наприклад:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let value of colors.values()) {
    console.log(value);
}

for (let value of tracking.values()) {
    console.log(value);
}

for (let value of data.values()) {
    console.log(value);
}

Цей код виведе таке:

"red"
"green"
"blue"
1234
5678
9012
"Understanding ECMAScript 6"
"ebook"

Виклик ітератора values(), як у цьому прикладі, повертає саме ті дані, що містяться у кожній з колекцій без будь–якої інформації про те, де ці дані знаходяться у колекції.

Ітератор keys()

Ітератор keys() повертає всі ключі, що присутні у колекції. Для масивів це лише числові ключі, а не власні властивості масиву. Для множин це ключі, які є одночасно значеннями, і тому keys() та values() повертають один і той же ітератор. Для мап ітератор keys() поверне кожен унікальний ключ. Ось приклад, що демонструє всі три випадки:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

for (let key of colors.keys()) {
    console.log(key);
}

for (let key of tracking.keys()) {
    console.log(key);
}

for (let key of data.keys()) {
    console.log(key);
}

Цей приклад дає такий вивід:

0
1
2
1234
5678
9012
"title"
"format"

Ітератор keys() зчитує кожен ключ у colors, tracking та data, і ці ключі виводяться у трьох циклах for-of. Для об’єктів–масивів лише числові ключі, навіть якщо ви додасте іменовані властивості у масив. Це відрізняється від того, як працює цикл for-in, тому що цикл for-in ітерується лише властивостями, що є числовими індексами.

Ітератори за замовчуванням для типів колекцій

Кожен тип колекцій також має свій власний ітератор за замовчування, що використовує for-of тоді, коли ітератор не вказаний безпосередньо. Метод values() є ітератором за замовчуванням для масивів та множин, тоді як метод entries() є ітератором за замовчування для мап. Це робить використання об’єктів–колекцій у циклах for-of трішки простішим. Для прикладу, розгляньте такий приклад:

let colors = [ "red", "green", "blue" ];
let tracking = new Set([1234, 5678, 9012]);
let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "print");

// те саме, що й при використанні colors.values()
for (let value of colors) {
    console.log(value);
}

// те саме, що й при використанні tracking.values()
for (let num of tracking) {
    console.log(num);
}

// те саме, що й при використанні data.entries()
for (let entry of data) {
    console.log(entry);
}

Ітератор не вказаний, тому використовується функцій–ітератор за замовчуванням. Ітератор за замовчування для масивів, множин та мап розроблений, щоб відображати те, як ці об’єкти ініціалізуються, тому такий код виводить ось це:

"red"
"green"
"blue"
1234
5678
9012
["title", "Understanding ECMAScript 6"]
["format", "print"]

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

A> Деструктурування та цикли for-of

A> Поведінка конструктора за замовчуванням для мап є дуже зручною при використанні циклу for-of з деструктуруванням, як у цьому прикладі:

let data = new Map();

data.set("title", "Understanding ECMAScript 6");
data.set("format", "ebook");

// те саме, що й при використанні data.entries()
for (let [key, value] of data) {
    console.log(key + "=" + value);
}

A> Цикл for-of у цьому коді використовує деструктурування масиву, щоб присвоювати значення key та value для кожного елементу у мапі. Таким чином, ви можете легко працювати з ключами та значеннями одночасно без потреби витягувати значення з двохелементного масиву або діставати з мапи ключ чи значення. Використання деструктивного масиву для мап робить використання циклу for-of для мап таким же зручним, як і для множин та масивів.

Рядкові ітератори

Починаючи з релізу ECMAScript 5, рядки у JavaScript поступово ставали схожими на масиви. Наприклад, ECMAScript 5 формалізував запис з квадратними дужками для отримання символу у рядках ((як от використання text[0], щоб отримати перший символ і т.д.) Але запис з квадратними дужками частіше працює з кодовими словами (code units), ніж з символами, тому він не може використовуватись для правильного отримання двохбайтних символів, що і демонструє цей приклад:

var message = "A 𠮷 B";

for (let i=0; i < message.length; i++) {
    console.log(message[i]);
}

Цей код використовує запис з квадратними дужками та властивість length для ітерації та виводу Unicode–символів, що містяться у рядку. Вивід буде дещо неочікуваний:

A
(порожній рядок)
(порожній рядок)
(порожній рядок)
(порожній рядок)
B

Оскільки двохбайтні символи трактуються як два окремих кодових слова, між A та B виводяться чотири порожні рядки.

На щастя, ECMAScript 6 націлений на повну підтримку Unicode (дивіться Главу 2), тому ітератор за замовчування для рядків пробує вирішити проблему з ітерацією рядками. Можна здогадатись, що ітератор за замовчуванням для рядків працює з символами замість кодових слів. Якщо використати у цьому прикладі ітератор за замовчуванням за рядків та цикл for-of, ми отримаємо більш прийнятний вивід. Ось виправлений код:

var message = "A 𠮷 B";

for (let c of message) {
    console.log(c);
}

Він виведе ось це:

A
(порожній рядок)
𠮷
(порожній рядок)
B

Такий результат більше відповідає тому, що ви могли б очікувати при роботі з символами: цикл успішно вивів Unicode–символ так само добре, як і решту інших.

Ітератори NodeList

Об’єктна модуль документа (Document Object Model (DOM)) має тип NodeList, що відповідає колекції елементів у документі. Для тих, хто пише JavaScript для запуску у веб–браузерах, розуміння відмінності між об’єктами NodeList та масивами завжди було складним. Як об’єкти NodeList, так і масиви використовують властивість length для відображення кількості елементів: вони також обоє використовують запис з квадратними дужками для доступу до окремих елементів. Внутрішньо, однак, NodeList та масив поводяться по–різному і це призводить до плутанини.

З введенням ітераторів за замовчуванням у ECMAScript 6, DOM визначення NodeList (більшою мірою це стосується специфікації HTML, ніж самого ECMAScript 6) включає ітератор за замовчуванням, що поводиться так само, як і ітератор за замовчуванням для масивів. Це означає, що ви можете використовувати NodeList у циклі for-of або в іншому місці, що використовує ітератор об’єкту за замовчуванням. Наприклад:

var divs = document.getElementsByTagName("div");

for (let div of divs) {
    console.log(div.id);
}

Цей код викликає getElementsByTagName() для отримання NodeList, що представляє всі елементи <div> у об’єкті document. Потім цикл for-of ітерується всіма елементами та виводить ID елемента, фактично роблячи код таким, наче він написаний для звичайного масиву.

Оператор розкладу та ітерабельні немасиви

Пригадаємо з Глави 7, що оператор розкладу (...) може використовуватись для перетворення множини у масив. Наприклад:

let set = new Set([1, 2, 3, 3, 3, 4, 5]),
    array = [...set];

console.log(array);             // [1,2,3,4,5]

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

let map = new Map([ ["name", "Nicholas"], ["age", 25]]),
    array = [...map];

console.log(array);         // [ ["name", "Nicholas"], ["age", 25]]

Тут оператор розкладу перетворює map у масив масивів. Оскільки ітератор за замовчування для мап повертає пари “ключ–значення”, отриманий масив виглядає так, як масив, який було передавали при виклику new Map().

Ви можете використовувати оператор розкладу у літералах масивів стільки разів, скільки потрібно, і ви можете використовувати його будь–де, де ви хочете вставити кілька елементів з ітерабельного об’єкта. Ці елементи просто з’являться у новому масиві у тому порядку, в якому вони знаходились у операторі розкладу. Наприклад:

let smallNumbers = [1, 2, 3],
    bigNumbers = [100, 101, 102],
    allNumbers = [0, ...smallNumbers, ...bigNumbers];

console.log(allNumbers.length);     // 7
console.log(allNumbers);    // [0, 1, 2, 3, 100, 101, 102]

Оператор розкладу використовується, щоб створити allNumbers зі значень у smallNumbers та bigNumbers. Значення вставляються у allNumbers у тій послідовності, в якій масиви були передані при створенні allNumbers: 0 — перший, за ним значення зі smallNumbers, а за ними значення з bigNumbers. Оригінальні масиви залишаються незміненими тому, що їхні значення просто скопіювались у allNumbers.

Оскільки оператор розкладу може використовуватись з будь–яким ітерабельним об’єктом, це найлегший спосіб конвертації ітерабельного об’єкту в масив. Ви можете сконвертувати рядки у масив з символів (не кодових слів) та об’єкти NodeList з браузеру у масив вузлів.

Тепер, коли ви зрозуміли основи роботи ітераторів, включаючи for-of та оператор розкладу, час подивитись на більш складні способи використання ітераторів.

Розширена функціональність ітераторів

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

Передача аргументів у інтератори

Протягом цієї глави, у прикладах, ітератори видавали значення з допомогою методу next() або з використанням yield у генераторі. Проте ви також можете передавати аргументи в ітератор через метод next(). Якщо передавати аргумент у метод next(), цей аргумент стане значенням оператора yield всередині генератора. Ця можливість дуже важлива для такого розширеного функціоналу як асинхронне програмування. Ось простий приклад:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // 4 + 2
    yield second + 3;                   // 5 + 3
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next(4));          // "{ value: 6, done: false }"
console.log(iterator.next(5));          // "{ value: 8, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

Перший виклик next() є спеціальними випадком: якщо передати у нього будь–який аргумент, він буде втрачений. Оскільки аргументи, що передаються у next(), стають значеннями, які повертає yield, аргумент для першого виклику next() міг би лише замінити перший оператор yield у функції–генераторі, якщо б він був доступний перед оператором yield. Це неможливо, і тому немає змісту передавати аргумент при першому виклику next().

При другому виклику next(), в якості аргументу передається значення 4. Змінній first всередині функції–генератора присвоюється 4. У операторі yield, включаючи присвоєння, права частина виразу обчислюється при першому виклику next(), а ліва чистина обчислюється при другому виклику next(), перед тим, як функція продовжить виконання. Оскільки другий виклик next() приймає 4, це значення присвоюється first і тоді виконання продовжується.

Другий yield використовує результат першого yield і додає два, і тому повертає значення шість. Коли у третє викликається next(), в якості аргументу передається значення 5. Це значення присвоюється змінній second, а тоді використовується у третьому операторі yield, щоб повернути 8.

Легше думати про те, що відбувається, якщо розглядати код, який виконується тоді, коли функція–генератор продовжує виконання. Зображення 8-1 показує який код виконується перед зупинками з допомогою кольорів.

Зображення 8-1: Виконання коду всередині генератора

Жовтий колір відображає перший виклик next() та весь код, що виконується в результаті. Блакитний колір відображає виклик next(4) і весь код, що виконується при цьому виклику. Рожевий колір відповідає виклику next(5) та коду, що виконується в результаті. Складним для розуміння є те, як код у правій частині кожного з виразів виконується і зупиняється перед тим, як виконується код у лівій частині. Це робить зневадження складних генераторів дещо складнішим, ніж налагодження звичайних функцій.

Ви вже побачили, що yield може поводитись як return, якщо передати у метод next() якесь значення. Однак, це не єдиний прийом, який ви можете використовувати всередині генератора. Ви також можете спровокувати ітератор кинути помилку.

Кидання помилок в ітераторах

В ітератор можливо передавати не лише дані, але й помилки. Для ітераторів можна імплементувати метод throw(), що буде вказувати ітератору чи кидати помилку, коли він продовжить виконання. Це також важлива можливість для асинхронного програмування, коли вам потрібно як повернення значень, так і кидання помилок (два способи виходу з функції). Ви можете передати об’єкт помилки у throw(), що буде кинутий, коли ітератор продовжить виконання. Наприклад:

function *createIterator() {
    let first = yield 1;
    let second = yield first + 2;       // yield 4 + 2, тоді кинути
    yield second + 3;                   // не виконається ніколи
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // помилка кидається в ітераторі

У цьому прикладі, перші два вирази з yield виконуються нормально, але коли викликається throw(), помилка кидається до того, як виконається let second. Це просто зупиняє виконання коду, так само як це би сталось, якщо кинути помилку безпосередньо. Єдина відмінність — це місце, де було кинуто помилку. Зображення 8-2 демонструє, який код виконується на кожному кроці.

Зображення 8-2: Кидання помилок всередині ітератора

На цьому зображення, червоний колір відповідає коду, що виконається, коли викличеться throw(), а червона зірка показує, де, приблизно, кинеться помилка всередині генератора. Перші два оператора yield виконуються, але коли викликається throw(), помилка кидається до того, як виконається будь–який інший код.

Знаючи це, ви можете ловити такі помилки всередині генератора з допомогою блоку try-catch:

function *createIterator() {
    let first = yield 1;
    let second;

    try {
        second = yield first + 2;       // yield 4 + 2, тоді кинути
    } catch (ex) {
        second = 6;                     // якщо помилка, присвоїти інше значення
    }
    yield second + 3;
}

let iterator = createIterator();

console.log(iterator.next());                   // "{ value: 1, done: false }"
console.log(iterator.next(4));                  // "{ value: 6, done: false }"
console.log(iterator.throw(new Error("Boom"))); // "{ value: 9, done: false }"
console.log(iterator.next());                   // "{ value: undefined, done: true }"

У цьому прикладі, блок try-catch огорнутий довкола другого оператора yield. Коли цей yield виконується без помилок, до того як second присвоїться будь–яке значення кидається, тому блок catch присвоює йому значення шість. Тоді виконання продовжується до наступного yield і повертає дев'ять.

Зверніть у вагу на те, що відбулось дещо цікаве: метод throw() повернув об’єкт–результат так само, як і метод next(). Оскільки помилка була впіймана всередині генератора, виконання коду продовжилось до наступного yield та повернуло наступне значення, 9.

Краще думати про next() та throw() як про інструкції для ітератора. Метод next() вказує ітератору продовжити виконання (можливо з вказаним значенням), а метод throw() наказує ітератору продовжити виконання киданням помилки. Що станеться після цього, залежить від коду всередині генератора.

Методи next() та throw() керуються ходом виконання всередині ітератора при використанні yield, проте ви також можете використовувати оператор return. Але return працює по—іншому, на відміну від того, як він працює у звичайних функціях. Як саме, ви побачите у наступному розділі.

Оператор return у генераторах

Оскільки генератори є функціями, ви можете використовувати оператор return для завчасного виходу та задання результату для останнього виклику методу next(). У більшості прикладів у цій главі, останній виклик next() для ітератора повертав undefined, проте ви можете задати інше значення з допомогою return так само, як ви могли б це зробити з будь–якою іншою функцією. У генераторі return вказує на те, що всі операції закінчено, тому властивості done встановлюється true, а значення, якщо воно вказане, стає полем value. Ось приклад, що просто закінчує виконання раніше з допомогою return:

function *createIterator() {
    yield 1;
    return;
    yield 2;
    yield 3;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

У цьому коді, генератор має оператор yield, за яким слідує оператор return. return вказує, що більше значення більше не надходитимуть, і тому решту інструкцій yield не виконуються (їх не можливо досягнути).

Ви також можете вказати значення, яке треба повернути, і яке стане полем value об’єкту–результату. Наприклад:

function *createIterator() {
    yield 1;
    return 42;
}

let iterator = createIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 42, done: true }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

Тут значення 42 повертається у полі value після другого виклику методу next() (перший — у якого поле done дорівнює true). Третій виклик next() повертає об’єкт, у якого властивість value знову дорівнює undefined. Будь–яке значення, яке ви вказали через return, доступне у об’єкті–результаті лише раз, перед тим, як поле value буде скинуто до значення undefined.

I> Оператор розкладу та for-of ігнорують будь–яке значення, що вказується з допомогою інструкції return. Як тільки вони бачать, що done дорівнює true, вони зупиняються без читання value. Однак, значення повернення у ітераторах корисні при делегуванні генераторів.

Делегування генераторів

У деяких випадках зручно скомбінувати значення з двох ітераторів у один. Генератори можна делегувати до інших генераторів з допомогою спеціальної форми yield з символом зірочки (*). Як і з визначенням генератора, не має значення, де стоїть зірочка, поки вона знаходиться між ключовим слово yield та ім’ям функції–генератора. Ось приклад:

function *createNumberIterator() {
    yield 1;
    yield 2;
}

function *createColorIterator() {
    yield "red";
    yield "green";
}

function *createCombinedIterator() {
    yield *createNumberIterator();
    yield *createColorIterator();
    yield true;
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "red", done: false }"
console.log(iterator.next());           // "{ value: "green", done: false }"
console.log(iterator.next());           // "{ value: true, done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

У цьому прикладі, генератор createCombinedIterator() генератор делегує спершу до createNumberIterator(), а тоді до createColorIterator(). Спільний ітератор ззовні виглядає так, наче це один спільний, що повертає всі ці значення. Кожен виклик next() делегується до потрібного ітератора, поки ітератори, що створені через createNumberIterator() та createColorIterator(), не стануть порожніми. Тоді останнє yield виконується, щоб повернути true.

Делегування ітераторів також дозволяє вам поглиблено використовувати значення, що повертаються з генератора. Це є найлегшим способом отримати доступ до таких значень виходу і може бути дуже корисним при виконанні складних завдань. Наприклад:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

Тут генератор createCombinedIterator() делегується до createNumberIterator() та присвоює повернуте значення у result. Оскільки createNumberIterator() містить return 3, повернутим значенням є 3. Тоді змінна result передається у createRepeatingIterator() в якості аргументу, що вказує, скільки разів треба повернути однаковий рядок (у цьому випадку, тричі).

Зауважте, що значення 3 ніколи не виводиться при будь–якому виклику методу next(). Просто зараз, воно існує виключно всередині генератора createCombinedIterator(). Проте, ви можете так само вивести це значення, просто додавши ще одну інструкцію yield, ось так:

function *createNumberIterator() {
    yield 1;
    yield 2;
    return 3;
}

function *createRepeatingIterator(count) {
    for (let i=0; i < count; i++) {
        yield "repeat";
    }
}

function *createCombinedIterator() {
    let result = yield *createNumberIterator();
    yield result;
    yield *createRepeatingIterator(result);
}

var iterator = createCombinedIterator();

console.log(iterator.next());           // "{ value: 1, done: false }"
console.log(iterator.next());           // "{ value: 2, done: false }"
console.log(iterator.next());           // "{ value: 3, done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: "repeat", done: false }"
console.log(iterator.next());           // "{ value: undefined, done: true }"

У цьому коді, додатковий оператор yield безпосередньо виводить повернене з генератора createNumberIterator() значення.

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

I> Ви можете використовувати yield * відразу над рядками (як от yield * "hello"), і тоді буде використаний ітератор за замовчуванням для рядків.

Асинхронний запуск завдань

Багато уваги довкола генераторів стосується асинхронного програмування. Асинхронне програмування у JavaScript — це палиця з двома кінцями: легко виконувати прості завдання асинхронно, але складні завдання змушують заглибитись в організацію коду. Оскільки генератори дозволяють вам зручно зупиняти код під час виконання, вони відкриваються широкі можливості, пов’язані з асинхронною обробкою.

Звичним способом виконати асинхронну операцію є виклик функції, що має зворотній виклик. Наприклад, розгляньте приклад читання файлу з диску у Node.js:

let fs = require("fs");

fs.readFile("config.json", function(err, contents) {
    if (err) {
        throw err;
    }

    doSomethingWith(contents);
    console.log("Done");
});

Метод fs.readFile() викликається з іменем файлу, який треба прочитати, та з функцією зворотнього виклику. Коли операція закінчиться, викличеться функція зворотнього виклику. Зворотній виклик перевіряє, чи сталась помилка, і, якщо не сталась, обробляє повернений contents. Це працює добре, коли ви маєте обмежену кількість асинхронних завдань, які потрібно виконати, проте зі зростанням складності, вам потрібно вкладати зворотні виклики один в одного, або послідовно виконувати серію асинхронних завдань. Саме тут генератори та yield стають в нагоді.

Простий обробник завдань

Оскільки yield зупиняє виконання і, щоб почати роботу, чекає на виклик методу next(), ви можете реалізувати асинхронні виклики без обробки зворотніх викликів. Для початку, вам потрібна функція, що викликає генератор і стартує ітератор, наприклад ось така:

function run(taskDef) {

    // створюємо ітератор, що буде доступний усюди
    let task = taskDef();

    // починаємо виконувати завдання
    let result = task.next();

    // рекурсивна функція для постійного виклику next()
    function step() {

        // якщо є що виконувати
        if (!result.done) {
            result = task.next();
            step();
        }
    }

    // починаємо процес
    step();

}

Функція run() приймає в якості аргументу означення завдання (функцію–генератор). Вона викликає генератор, щоб створити ітератор та зберігає цей ітератор у task. Змінна task знаходиться поза функцією, тому вона доступна іншим функціям (я поясню чому пізніше у цьому розділі). Перший виклик next() починає ітерування, і результат зберігається для подальшого використання. Функція step() перевіряє чи result.done рівне false, і, якщо так, то викликає next() перед рекурсивним викликом самої себе. Кожен виклик next() зберігає повернуте значення у result, який щоразу перезаписується, щоб містити актуальну інформацію. Перший виклик step() починає процес перевірки змінної result.done, щоб бачити чи є робота, яку потрібно виконати.

З такою імплементацією run(), ви можете запускати генератор з кількома інструкціями yield, ось так:

run(function*() {
    console.log(1);
    yield;
    console.log(2);
    yield;
    console.log(3);
});

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

Обробка завдань з даними

Найпростішим способом передати дані через обробник завдань є передача значення визначеного через yield у наступний виклик методу next(). Щоб зробити це, вам потрібно просто передавати result.value, як у цьому коді:

function run(taskDef) {

    // створюємо ітератор, що буде доступний усюди
    let task = taskDef();

    // починаємо виконувати завдання
    let result = task.next();

    // рекурсивна функція для постійного виклику next()
    function step() {

        // якщо є що виконувати
        if (!result.done) {
            result = task.next(result.value);
            step();
        }
    }

    // починаємо процес
    step();

}

Тепер result.value передається у next() як аргумент, і тому можливо передавати дані між викликами yield, як ось тут:

run(function*() {
    let value = yield 1;
    console.log(value);         // 1

    value = yield value + 3;
    console.log(value);         // 4
});

Цей приклад виводить два значення у консоль: 1 та 4. Значення 1 приходить з yield 1, тому що 1 передається назад у змінну value. 4 обчислюється в результаті додавання 3 до value та передачі цього результату назад у value. Тепер дані передаються між викликами yield — вам потрібно зробити лише одну маленьку зміну для того, щоб обробляти асинхронні виклики.

Асинхронний обробник завдань

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

Ось один спосіб, яким ви можете просигналізувати, що значення є асинхронною операцією:

function fetchData() {
    return function(callback) {
        callback(null, "Hi!");
    };
}

З цього прикладу слід розуміти, що будь–яка функція, яка має викликатись обробником завдань має повернути функцію, що виконує зворотній виклик. Функція fetchData() повертає функцію, що приймає функцію зворотнього виклику в якості аргументу. Коли повернена функція викликається, вона виконує фунцію зворотнього виклику з одним шматком даних (рядком "Hi!"). Аргумент callback повинен прийти з обробника завдань для впевненості у тому, що зворотній виклик, який виконується, правильно взаємодіє з відповідним ітератором. Функція fetchData() є синхронною, але ви легко можете зробити її асинхронною зробивши зворотній виклик з невеликою затримкою, ось так:

function fetchData() {
    return function(callback) {
        setTimeout(function() {
            callback(null, "Hi!");
        }, 50);
    };
}

Така версія fetchData() вносить затримку на 50 мс перед зворотним викликом, демонструючи тим самим, що такий шаблон працює однаково добре як для синхронного, так і для асинхронного коду. Ви просто маєте бути певні, що кожна функція, що має викликатись з yield, відповідає цьому шаблону.

З хорошим розумінням того, як функція може сигналізувати про те, що вона є асинхронною, ви можете змінити обробник завдань так, щоб він враховував таку поведінку. Щоразу, коли result.value є фунцією, обробник завдань виконуватиме її, замість того, щоб просто передати значення у метод next(). Ось оновлений код:

function run(taskDef) {

    // створюємо ітератор, що буде доступний усюди
    let task = taskDef();

    // починаємо виконувати завдання
    let result = task.next();

    // рекурсивна функція для постійного виклику next()
    function step() {

        // якщо є що виконувати
        if (!result.done) {
            if (typeof result.value === "function") {
                result.value(function(err, data) {
                    if (err) {
                        result = task.throw(err);
                        return;
                    }

                    result = task.next(data);
                    step();
                });
            } else {
                result = task.next(result.value);
                step();
            }

        }
    }

    // починаємо процес
    step();

}

Якщо result.value є функцією (перевіряється оператором ===), тоді вона викликається з функцією зворотнього виклику. Ця функція зворотнього виклику відповідає запису прийнятому в Node.js: коли будь–яка можлива помилка передається в якості першого аргументу (err), а результат у якості другого. Якщо присутня змінна err, це означає, що з'явилась помилка і тоді, замість task.next(), викликається task.throw() з об’єктом помилки, тому помилка видається у правильному місці. Якщо помилки немає, тоді data передається у task.next(), а результат зберігається. Тоді, для продовження процесу, викликається step(). Якщо result.value не є функцією, тоді воно відразу передається у метод next().

Ця нова версія обробника завдань готова для обробки всіх асинхронних завдань. Для читання даних з файлу у Node.js, вам потрібно зробити обгортку довкола fs.readFile(), що поверне функцію схожу на функцію fetchData() на початку цього розділу. Наприклад:

let fs = require("fs");

function readFile(filename) {
    return function(callback) {
        fs.readFile(filename, callback);
    };
}

Метод readFile() приймає єдиний аргумент, ім’я файлу, та повертає функцію, що викликає функцію зворотнього виклику. Функція зворотнього виклику передається у метод fs.readFile(), який виконає її після закінчення операції. Ви можете виконати це завдання з допомогою yield ось так:

run(function*() {
    let contents = yield readFile("config.json");
    doSomethingWith(contents);
    console.log("Done");
});

Цей приклад виконує асинхронну операцію readFile() без видимих функцій зворотнього виклику в основному коді. Біля yield код виглядає так само, як і синхронний код. Поки функції виконують асинхронні операції з усім відповідним інтерфейсом, ви можете писати логіку, що читається як синхронний код.

Звісно є недоліки підходу, що використовувався у цих прикладах, зокрема, ви не завжди можете бути певні, що функція, що повертає функцію є асинхронною. Однак, зараз найважливіше те, що ви зрозуміли теорію, на якій базується такий обробник завдань. Використання промісів надає більш потужний спосіб планування асинхронних завдань, і Глава 11 розповість про це детальніше.

Підсумок

Ітератори є важливою частиною ECMAScript 6 і є коренем для кількох ключових елементів мови. У простих випадках ітератори надають просту можливість повертати послідовності значень, використовуючи простий API. Однак, є набагато більше складніших способів використання ітераторів у ECMAScript 6.

Символ Symbol.iterator використовується для визначення ітератора за замовчуванням для об’єктів. Як вбудовані, так і створені розробниками об’єкти можуть використовувати цей символ, щоб передати метод, що повертатиме ітератор. Якщо Symbol.iterator переданий об’єкту, тоді цей об’єкт вважається ітерабельним.

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

Щоб зробити використання for-of легшим, багато значень у ECMAScript 6 мають ітератори за замовчуванням. Всі типи колекцій (масиви, мапи та множини) мають ітератори, які розроблені так, щоб зробити їхній вміст доступнішим. Рядки також мають ітератор за замовчуванням, який робить простішим ітерування символами у рядку (замість ітерування кодовими словами).

Оператор розкладу працює з будь–якими ітерабельними об’єктами і робить простішою їх конвертацію у масиви. Конвертація відбувається шляхом читання значень з ітератора та вставляння кожного значення у масив.

Генератор — це спеціальна функція, яка при виклику автоматично створює ітератор. Оголошення генераторів позначаються символом зірочки (*) та використанням ключового слова yield, які показують, яке значення потрібно повертати при кожному успішному виклику методу next().

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

Можливо, найбільш цікавим та очікуваним аспектом генераторів та ітераторів є можливість створення асинхронного коду, який виглядатиме більш чистішим. Замість необхідності у використанні зворотніх викликів, ви можете написати код, який виглядатиме синхронно, але насправді використовуватиме yield, щоб чекати на завершення асинхронних операцій.

results matching ""

    No results matching ""