Звичайні функції повертають лише одне, єдине значення (або нічого).
Генератори можуть повертати (“yield”) кілька значень, одне за одним, на вимогу. Вони чудово працюють з об’єктами, що перебираються, дозволяючи легко створювати потоки даних.
Функції-генератори
Щоб створити генератор, нам потрібна спеціальна синтаксична конструкція: function*, так звана “функція-генератор”.
Це виглядає ось так:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
Функції-генератори поводяться інакше, ніж звичайні. Коли така функція викликається, вона не запускає свій код. Замість цього вона повертає спеціальний об’єкт, який називається «об’єкт-генератор», щоб керувати її виконанням.
Ось подивіться:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "функція-генератор" створює "об’єкт-генератор"
let generator = generateSequence();
alert(generator); // [object Generator]
Виконання коду функції ще не розпочато:
Основним методом генератора є next(). При виклику він запускає виконання коду до найближчого оператора yield <value> (value можна опустити, тоді воно є undefined). Потім виконання функції призупиняється, а отримане value повертається до зовнішнього коду.
Результатом next() завжди є об’єкт з двома властивостями:
value: отримане значення.done:true, якщо код функції закінчився, інакшеfalse.
Наприклад, тут ми створюємо генератор і отримуємо його перше значення, що повертається:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
На даний момент ми отримали лише перше значення, а виконання функції відбувається на другому рядку:
Давайте знову викличемо generator.next(). Він відновлює виконання коду і повертає наступний yield:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
І якщо ми викликаємо його втретє, виконання досягає оператора return, який завершує виконання функції:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
Тепер генератор виконався. Ми можемо побачити це за допомогою done:true і обробити value:3 як кінцевий результат.
Нові виклики generator.next() більше не мають сенсу. Якщо ми їх робимо, вони повертають той самий об’єкт: {done: true}.
function* f(…) чи function *f(…)?Обидва синтаксиси правильні.
Але зазвичай перевага віддається першому синтаксису, оскільки зірочка * означає, що це функція-генератор, вона описує вид, а не ім’я, тому її слід розташовувати разом із ключовим словом function.
Перебір генераторів
Як ви, напевно, вже здогадалися, дивлячись на метод next(), генератори є об’єктами, що перебираються.
Ми можемо перебирати їх значення за допомогою for..of:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, потім 2
}
Виглядає набагато приємніше, ніж виклик .next().value, чи не так?
…Але зверніть увагу: у прикладі вище показано 1, потім 2, і це все. Значення 3 не показується!
Це тому, що перебір через for..of ігнорує останнє value, коли done: true. Отже, якщо ми хочемо, щоб усі результати відображалися через for..of, то повинні повертати їх через yield:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1, потім 2, потім 3
}
Оскільки генератори є об’єктами, що перебираються, ми можемо використовувати всю пов’язану з ними функціональність, наприклад синтаксис розширення ...:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
У наведеному вище коді ...generateSequence() перетворює об’єкт-генератор, що перебирається, в масив елементів (докладніше про синтаксис розширення читайте у главі Залишкові параметри та синтаксис поширення)
Використання генераторів для об’єктів, що перебираються
Деякий час тому в главі Ітеративні об’єкти ми створили об’єкт range, що перебирається та повертає значення from..to.
Ось, давайте згадаємо код:
let range = {
from: 1,
to: 5,
// for..of range викликає цей метод один раз на самому початку
[Symbol.iterator]() {
// ...він повертає об’єкт, що перебирається:
// далі for..of працює лише з цим об’єктом, запитуючи в нього наступні значення
return {
current: this.from,
last: this.to,
// next() викликається при кожній ітерації цикла for..of
next() {
// повинно повернути значення як об’єкт {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
// при переборі об’єкта range повертаються числа від range.from до range.to
alert([...range]); // 1,2,3,4,5
Ми можемо використовувати функцію-генератор для перебору об’єкта, вказавши її як Symbol.iterator.
Ось той самий range, але набагато компактніший:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // скорочення для [Symbol.iterator]: function*()
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
alert( [...range] ); // 1,2,3,4,5
Це працює, тому що range[Symbol.iterator]() тепер повертає генератор, а методи генератора – це саме те, що очікує for..of:
- він має метод
.next() - повертає значення у вигляді
{value: ..., done: true/false}
Це, звичайно, не випадковість. Генератори були додані до мови JavaScript з урахуванням об’єктів, що перебираються, щоб їх було легше реалізувати.
Варіант з генератором набагато лаконічніший, ніж оригінальний код range, що перебирається, і зберігає ту саму функціональність.
У вищенаведених прикладах ми створили кінцеві послідовності, але ми також можемо створити генератор, який видає значення нескінченно. Наприклад, нескінченна послідовність псевдовипадкових чисел.
Безсумнівно, для цього буде потрібно break (або return) у циклі for..of в такому генераторі. Інакше цикл повторюватиметься нескінченно та зависне.
Композиція генераторів
Композиція генераторів – це особливість генераторів, що дозволяє прозоро “вбудовувати” генератори один в одного.
Наприклад, у нас є функція, яка генерує послідовність чисел:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
Тепер ми хотіли б повторно використати його для створення складнішої послідовності:
- спочатку цифри
0..9(з кодами символів 48…57), - за якими йдуть великі літери алфавіту
A..Z(коди символів 65…90) - за якими йдуть малі літери алфавіту
a..z(коди символів 97…122)
Ми можемо використовувати цю послідовність, наприклад створювати паролі, вибираючи з неї символи (можна також додати символи пунктуації), але давайте спочатку згенеруємо її.
У звичайній функції, щоб об’єднати результати кількох інших функцій, ми викликаємо їх, зберігаємо результати, а потім об’єднуємо в кінці.
Для генераторів існує спеціальний синтаксис yield* для “вбудовування” (компонування) одного генератора в інший.
Ось композиція генераторів:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generatePasswordCodes() {
// 0..9
yield* generateSequence(48, 57);
// A..Z
yield* generateSequence(65, 90);
// a..z
yield* generateSequence(97, 122);
}
let str = '';
for(let code of generatePasswordCodes()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
Директива yield* делегує виконання іншому генератору. Цей термін означає, що yield* gen виконує ітерацію над генератором gen і прозоро передає його вихід назовні. Ніби значення були отримані зовнішнім генератором.
Результат такий самий, як якби ми вставили код із вкладених генераторів: