Генераторы представляют особый тип функции, которые используются для генерации значений. Для определения генераторов применяется символ звездочки *, который ставится после ключевого слова
function
. Например, определим простейший генератор:
function* getNumber(){ yield 5; } const numberGenerator = getNumber(); const result = numberGenerator.next(); console.log(result); // {value: 5, done: false}
Здесь функция getNumber()
представляет генератор. Основные моменты создания и применения генератора:
Генератор определяется как функция с помощью оператора function* (символ звездочки после слова function
)
function* getNumber(){ .... }
Функция генератора возвращает итератор.
Для возвращения значения из генератора, как и вообще в итераторах, применяется оператор yield, после которого указывается возвращаемое значение
yield 5;
То есть фактически в данном случае генератор getNumber()
генерирует число 5.
Для получения значения из генератора применяется оператор применяется метод next()
const result = 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(); console.log(numberGenerator.next().value); // 5 console.log(numberGenerator.next().value); // 25
При этом важно понимать, что между двумя последовательными вызовами next()
может пройти некоторое неопределенное время, между ними могут располагаться какие-то другие действия,
и все равно генератор будет возвращать свое следующее значение.
const numberGenerator = getNumber(); console.log(numberGenerator.next().value); // 5 // ряд других действий console.log(numberGenerator.next().value); // 25
Генератор необязательно содержит только определение операторов 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}
Как выше мы увидели, каждый последующий вызов next()
возвращает следующее значение генератора, однако мы можем завершить работу генератора с помощью
метода return():
function* getNumber(){ yield 5; yield 25; yield 125; } const numberGenerator = getNumber(); console.log(numberGenerator.next()); // {value: 5, done: false} numberGenerator.return(); // завершаем работу генератора console.log(numberGenerator.next()); // {value: undefined, done: true}
Поскольку для получения значений применяется итератор, то мы можем использовать цикл 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() можно передать в генератор данные. Переданные в этот метод данные можно получить в функции генератора через предыдущий вызов оператора yield:
function* getNumber(){ const n = yield 5; // получаем значение numberGenerator.next(2).value console.log("n:", n); const m = yield 5 * n; // получаем значение numberGenerator.next(3).value console.log("m:", m); yield 5 * m; } const numberGenerator = getNumber(); console.log(numberGenerator.next().value); // 5 console.log(numberGenerator.next(2).value); // 10 console.log(numberGenerator.next(3).value); // 15
Консольный вывод:
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(); console.log(personGenerator.next()); // {value: "Tom", done: false} personGenerator.throw("Something wrong"); // Error: Something wrong console.log(personGenerator.next()); // {value: undefined, done: true}
Прежде всего в функции генератора для обработки возможного исключения используем конструкцию 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}