Генераторы
Генераторы представляют особый тип функции, которые используются для генерации значений. Для определения генераторов применяется символ звездочки *
, который ставится после ключевого слова function
. Например, определим простейший генератор:
function* getNumber(){
yield 5;
}
const numberGenerator = getNumber();
const next = numberGenerator.next();
// {value: 5, done: false}
console.log(next);
Здесь функция getNumber()
представляет генератор. Основные моменты создания и применения генератора:
- Генератор определяется как функция с помощью оператора
function*
. Функция генератора возвращает итератор - Для возвращения значения из генератора, как и вообще в итераторах, применяется оператор
yield
, после которого указывается возвращаемое значениеyield 5;
, фактически в данном случае генераторgetNumber()
генерирует число5
- Для получения значения из генератора применяется оператор применяется метод
next()
,const next = numberGenerator.next();
, в примере с помощью вызова функцииgetNumber()
создается объект итератора в виде константаnumberGenerator
. Используя этот объект, мы можем получать из генератора значения
Если мы посмотрим на консольный вывод, то мы увидим, что данный метод возвращает следующие данные:
{value: 5, done: false}
То есть по сути возвращается объект, свойство value
которого содержит собственно сгенерированное значение. А свойство done
указывает, достигли ли мы конца генератора.
Теперь изменим код:
function* getNumber(){
yield 5;
}
const numberGenerator = getNumber();
let next = numberGenerator.next();
console.log(next);
next = numberGenerator.next();
console.log(next);
Здесь обращение к методу next() происходит два раза:
{value: 5, done: false}
{value: undefined, done: true}
Функция генератора getNumber
генерирует только одно значение - число 5
. Поэтому при повторном вызове свойство value
будет иметь значение undefined
, а свойство done
- true
, то есть работа генератора завершена.
Генератор может создавать/генерировать множество значений:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
console.log(numberGenerator.next());
Консольный вывод:
{value: 5, done: false}
{value: 25, done: false}
{value: 125, done: false}
{value: undefined, done: true}
То есть при первом вызове метода next()
из итератора извлекается значение, которое идет после первого оператора yield
, при втором вызове метода next()
- значение после второго оператора yield
и так далее.
Для упрощения мы можем возвращать в генераторе элементы из массива:
const numbers = [5, 25, 125, 625];
function* getNumber(){
for(const n of numbers){
yield n;
}
}
const numberGenerator = getNumber();
// 5
console.log(numberGenerator.next().value);
// 25
console.log(numberGenerator.next().value);
При этом важно понимать, что между двумя последовательными вызовами next()
может пройти некоторое неопределенное время, между ними могут располагаться какие-то другие действия и все равно генератор будет возвращать свое следующее значение:
const numberGenerator = getNumber();
// 5
console.log(numberGenerator.next().value);
// 25
console.log(numberGenerator.next().value);
Генератор необязательно содержит только определение операторов yield
. Он также может содержать более сложную логику. С помощью генераторов удобно создавать бесконечные последовательности:
function* points(){
let x = 0;
let y = 0;
while(true){
yield {x:x, y:y};
x += 2;
y += 1;
}
}
let pointGenerator = points();
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
console.log(pointGenerator.next().value);
Консольный вывод:
{x: 0, y: 0}
{x: 2, y: 1}
{x: 4, y: 2}
Возвращение из генератора и функция return
Как выше мы увидели, каждый последующий вызов next()
возвращает следующее значение генератора, однако мы можем завершить работу генератора с помощью метода return()
:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
// {value: 5, done: false}
console.log(numberGenerator.next());
// завершаем работу генератора
numberGenerator.return();
// {value: undefined, done: true}
console.log(numberGenerator.next());
Получение значений генератора в цикле
Поскольку для получения значений применяется итератор, то мы можем использовать цикл for...of
:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
for(const num of numberGenerator){
console.log(num);
}
Консольный вывод:
5
25
125
Также мы можем применять и другие типы циклов, например цикл while
:
function* getNumber(){
yield 5;
yield 25;
yield 125;
}
const numberGenerator = getNumber();
while(!(item = numberGenerator.next()).done){
console.log(item.value);
}
Передача данных в генератор
Инициализация генератора
Функция генератора, как и любая другая функция, может принимать параметры. Соответственно через параметры мы можем передать в генератор некоторые данные:
function* getNumber(start, end, step){
for(let n = start; n <= end; n +=step){
yield n;
}
}
const numberGenerator = getNumber(0, 8, 2);
for(num of numberGenerator){
console.log(num);
}
Здесь в функцию генератора передается начальное конечное значения и шаг приращенния чисел:
0
2
4
6
8
Другой пример - определим генератор, который возвращет данные из массива:
function* generateFromArray(items){
for(item of items)
yield item;
}
const people = ["Tom", "Bob", "Sam"];
const personGenerator = generateFromArray(people);
for(person of personGenerator)
console.log(person);
В данном случае в генератор передается массив, который используется для генерации значений:
Tom
Bob
Sam
Передача данных в метод next
С помощью next()
можно передать в генератор данные. Переданные в этот метод данные можно получить в функции генератора через предыдущий вызов оператора yield
:
function* getNumber(){
// получаем значение numberGenerator.next(2).value
const n = yield 5;
console.log("n:", n);
// получаем значение numberGenerator.next(3).value
const m = yield 5 * n;
console.log("m:", m);
yield 5 * m;
}
const numberGenerator = getNumber();
// 5
console.log(numberGenerator.next().value);
// 10
console.log(numberGenerator.next(2).value);
// 15
console.log(numberGenerator.next(3).value);
Консольный вывод:
5
n: 2
10
m: 3
15
При втором вызове метода next()
:
numberGenerator.next(2).value
Мы можем получить переданные через него данные, присвоив результат первого вызова оператора yield
:
const n = yield 5;
То есть здесь константа n
будет равна 2
, так как в метод next()
передается число 2
. Далее мы можем использовать это значение, например, для генерации нового значения:
const m = yield 5 * n;
Соответственно, константа m
получит значение, переданное через третий вызов метода next()
, то есть число 3
.
Обработка ошибок генератора
С помощью функции throw()
мы можем сгенерировать в генераторе исключение. В качестве параметра в эту функцию передается произвольное значение, которое представляет информацию об ошибке:
function* generateData(){
try {
yield "Tom";
yield "Bob";
yield "Hello Work";
}
catch(error) {
console.log("Error:", error);
}
}
const personGenerator = generateData();
// {value: "Tom", done: false}
console.log(personGenerator.next());
// Error: Something wrong
personGenerator.throw("Something wrong");
// {value: undefined, done: true}
console.log(personGenerator.next());
Прежде всего в функции генератора для обработки возможного исключения используем конструкцию try..catch
. В блоке catch
с помощью параметра error мы можем получить информацию об ошибке, которая передается в функцию throw()
.
Далее при использовании генератора мы можем вызвать эту функцию, передавая в нее произвольную информацию об ошибке (в данном случае это просто некоторое строковое сообщения):
personGenerator.throw("Something wrong");
В итоге этот вызов приведет к генерации исключения в функции генератора, и управление перейдет к блоку catch
, который выводит информацию об ошибке на консоль:
catch(error) {
console.log("Error:", error);
}
Консольный вывод программы:
{value: "Tom", done: false}
Error: Something wrong
{value: undefined, done: true}
Стоит отметить, что после вызова функции throw()
генератор завершает работу, а далее при вызове метода next()
мы получим результат {value: undefined, done: true}
.