Кратко
СкопированоГенератор — это синтаксический сахар для создания особого вида объекта-итератора, который, помимо метода next, реализует два дополнительных метода throw и return.
Чтобы создать такой объект, нужно использовать функцию-генератор. Для её объявления к названию функции в начале добавляют символ звёздочки *.
Вызов функции вернёт объект-генератор, который одновременно будет итератором и итерируемым объектом (иметь свойство Symbol). У объекта-генератора есть пять возможных состояний: undefined, suspended, suspended, executing и completed. Нам доступно только три: suspended — приостановлен, executing — выполняется, close — завершён.
Для возврата значений используются операторы yield или yield*. Они приостанавливают выполнение функции с полным сохранением промежуточных вычислений.
Оператор yield* перенаправляет итерации в другой генератор. Мы как бы делаем спред другого генератора внутри нашего, получаем его значения и возвращаем их.
Вызов метода return завершает итерации и возвращает значение. Вызов метода throw завершает итерации и бросает ошибку.
Пример
СкопированоСоздаём функцию-генератор.
function* getLangs() { yield 'java'; debugger; yield 'js'; yield 'rust';}
function* getLangs() {
yield 'java';
debugger;
yield 'js';
yield 'rust';
}
Вызов функции вернёт объект-генератор.
const generator = getLangs()
const generator = getLangs()
Вызываем метод next, чтобы получить следующее значение:
generator.next()// { value: 'java', done: false }generator.next()// { value: 'js', done: false }generator.next()// { value: 'rust', done: false }generator.next()// { value: undefined, done: true }
generator.next()
// { value: 'java', done: false }
generator.next()
// { value: 'js', done: false }
generator.next()
// { value: 'rust', done: false }
generator.next()
// { value: undefined, done: true }
Так как генератор это ещё и итерируемый объект, то можно использовать его в цикле for.
Проверим, что генератор действительно итерируемый объект:
const generator = getLangs()console.log(generator[Symbol.iterator]() === generator)// true
const generator = getLangs()
console.log(generator[Symbol.iterator]() === generator)
// true
А теперь попробуем обойти его в цикле:
const generator = getLangs()for (const value of generator) { console.log(value)}// 'java'// 'js'// 'rust'
const generator = getLangs()
for (const value of generator) {
console.log(value)
}
// 'java'
// 'js'
// 'rust'
Как пишется
СкопированоЧтобы создать функцию-генератор, нужно добавить знак звёздочки между ключевым словом function и названием функции. Как именно ставить звёздочку — неважно.
function* generator() {}function * generator() {}function *generator() {}
function* generator() {}
function * generator() {}
function *generator() {}
Чтобы вернуть значение, используется оператор yield.
function* generator() { yield 1 yield 2}
function* generator() {
yield 1
yield 2
}
Обратите внимание, что вызывать return в генераторе необязательно. Если return нет, то, после выполнения всех yield, следующий вызов next вернёт { value.
const g = generator()g.next()// { value: 1, done: false }g.next()// { value: 1, done: false }g.next()// { value: undefined, done: true }
const g = generator()
g.next()
// { value: 1, done: false }
g.next()
// { value: 1, done: false }
g.next()
// { value: undefined, done: true }
Как понять
СкопированоЧем функция-генератор отличается от обычной функции? Функции в JavaScript выполняются полностью, и в конце мы ожидаем получить результат.
function createFullName(firstName, secondName) { return `${firstName} ${secondName}`}const fullName = createFullName('Анна', 'Каренина')console.log(fullName)// Анна Каренина
function createFullName(firstName, secondName) {
return `${firstName} ${secondName}`
}
const fullName = createFullName('Анна', 'Каренина')
console.log(fullName)
// Анна Каренина
Функция-генератор возвращает объект-генератор. Из этого объекта можно получать данные, вызывая метод next. При этом выполнение функции в буквальном смысле остановится.
function imaginaryHeavyComputation() { let result = 0 for (let i = 0; i < 100; i++) { result += i } return result}function* getLangs() { const result1 = imaginaryHeavyComputation() console.log('result of heavy compuation #1:', result1) yield 'java'; const result2 = imaginaryHeavyComputation() console.log('result of heavy compuation #2:', result1 + result2) yield 'js'; console.log("easy compuation:", 2 + 2) yield 'rust';}const generator = getLangs()// Никаких логов и вызовов функций не произошло
function imaginaryHeavyComputation() {
let result = 0
for (let i = 0; i < 100; i++) {
result += i
}
return result
}
function* getLangs() {
const result1 = imaginaryHeavyComputation()
console.log('result of heavy compuation #1:', result1)
yield 'java';
const result2 = imaginaryHeavyComputation()
console.log('result of heavy compuation #2:', result1 + result2)
yield 'js';
console.log("easy compuation:", 2 + 2)
yield 'rust';
}
const generator = getLangs()
// Никаких логов и вызовов функций не произошло
Генераторы по умолчанию ленивые. До тех пор, пока не будет вызван метод next, у возвращаемого объекта-генератора не будут происходить никакие вычисления. Но, даже после вызова next, выполнение функции произойдёт только до первого вызова yield. Если вызвать next ещё раз, то выполнение продолжится до следующего yield и так далее. Продолжим пример выше.
console.log(generator.next())// 'result of heavy compuation #1: 4950'// { value: 'java', done: false }console.log(generator.next())// 'result of heavy compuation #2: 9900'// { value: 'js', done: false }console.log(generator.next())// 'easy compuation: 4'// { value: 'rust', done: false }
console.log(generator.next())
// 'result of heavy compuation #1: 4950'
// { value: 'java', done: false }
console.log(generator.next())
// 'result of heavy compuation #2: 9900'
// { value: 'js', done: false }
console.log(generator.next())
// 'easy compuation: 4'
// { value: 'rust', done: false }
Таким образом, мы получили функцию, которая выполняется частями. Если вывести в консоль содержимое, можно лучше понять, что происходит внутри.
const generator = getLangs()console.log(generator)/*[[GeneratorLocation]]: VM229:1[[Prototype]]: Generator[[GeneratorState]]: "suspended"[[GeneratorFunction]]: ƒ* getLangs()[[GeneratorReceiver]]: Window[[Scopes]]: Scopes[3]*/
const generator = getLangs()
console.log(generator)
/*
[[GeneratorLocation]]: VM229:1
[[Prototype]]: Generator
[[GeneratorState]]: "suspended"
[[GeneratorFunction]]: ƒ* getLangs()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
*/
Вначале генератор находится в состоянии suspended, т. е. он приостановлен. Дальнейшие вызовы next тоже будут переводить генератор в это состояние до тех пор, пока генератор не вернёт все значения (пройдёт все вызовы yield). Генератор закроется, только когда вызов метода next вернёт объект с полем done.
generator.next()// { value: 'java', done: false }generator.next()// { value: 'js', done: false }generator.next()// { value: 'rust', done: false }generator.next()// { value: undefined, done: true }console.log(generator)/*[[GeneratorLocation]]: VM229:1[[Prototype]]: Generator[[GeneratorState]]: "closed" // Обратите внимание на изменившийся статус[[GeneratorFunction]]: ƒ* getLangs()[[GeneratorReceiver]]: Window[[Scopes]]: Scopes[3]*/
generator.next()
// { value: 'java', done: false }
generator.next()
// { value: 'js', done: false }
generator.next()
// { value: 'rust', done: false }
generator.next()
// { value: undefined, done: true }
console.log(generator)
/*
[[GeneratorLocation]]: VM229:1
[[Prototype]]: Generator
[[GeneratorState]]: "closed" // Обратите внимание на изменившийся статус
[[GeneratorFunction]]: ƒ* getLangs()
[[GeneratorReceiver]]: Window
[[Scopes]]: Scopes[3]
*/
Передача значений в генератор с yield
СкопированоВместе с генераторами в JavaScript был введён оператор yield. Как мы видели в примерах выше, yield приостанавливает функцию-генератор и возвращает значение. Можно представлять yield как двусторонний канал общения с генератором. С одной стороны мы получаем результат, с другой, можем передать значение в генератор в любой момент.
Добавим в предыдущий пример условие, что, если нам понравился первый язык программирования, мы учим другой похожий язык вместо JavaScript.
function* getLangs() { /** * Первый вызов next в любом случае вернёт 'java', * не имеет значения, передадим мы что-то в него или нет * * Переменная isFavorite при этом будет 'undefined' */ const isFavorite = yield 'java'; /** * Если мы передадим аргумент в 'next' при следующем вызове, то: * * 1) он будет присвоен переменной isFavorite; * 2) условие будет верно, и мы получим значение 'kotlin' */ if (isFavorite) { yield 'kotlin' } else { /** * или 'js', если вызовем 'next' без аргументов */ yield 'js'; } yield 'rust';}const generator = getLangs()generator.next()// { value: 'java', done: false }// Передаём true, потому что нам понравился Javagenerator.next(true)// { value: 'kotlin', done: false }
function* getLangs() {
/**
* Первый вызов next в любом случае вернёт 'java',
* не имеет значения, передадим мы что-то в него или нет
*
* Переменная isFavorite при этом будет 'undefined'
*/
const isFavorite = yield 'java';
/**
* Если мы передадим аргумент в 'next' при следующем вызове, то:
*
* 1) он будет присвоен переменной isFavorite;
* 2) условие будет верно, и мы получим значение 'kotlin'
*/
if (isFavorite) {
yield 'kotlin'
} else {
/**
* или 'js', если вызовем 'next' без аргументов
*/
yield 'js';
}
yield 'rust';
}
const generator = getLangs()
generator.next()
// { value: 'java', done: false }
// Передаём true, потому что нам понравился Java
generator.next(true)
// { value: 'kotlin', done: false }
Может показаться нелогичным, что при первом вызове next значение аргумента не запишется. Такое поведение генераторов связано с их «ленивостью». Первый вызов next можно считать инициализацией.
Если представить генератор как закрытую коробку, то первый вызов next — это как вытянуть первый предмет вслепую. Заранее неизвестно, что мы получим, и потому нельзя заранее сказать, что предмет нам понравится. Аналогично и в примере выше. Сначала мы хотим получить результат, а затем, на его основе, можем решить, какой аргумент передать в следующий вызов next.
Так что мы не можем передать значение в is в первом вызове next, но можем в следующем. Сначала генератор вернёт значение, а только потом запишет переданный ему аргумент.
Используя возможность передачи данных в генератор, можно по ходу его выполнения менять возвращаемые значения и создавать очень гибкие конструкции.
Вызов генераторов внутри генератора
СкопированоЕсли к вызову оператора yield добавить звёздочку *, то можно перенаправить выполнение в другой генератор.
Снова дополним наш пример и предположим, что, если нам понравился язык java, то мы хотим попробовать несколько языков на базе JVM.
function* jvmLangs() { yield 'kotlin' yield 'scala' yield 'closure'}function* getLangs() { const isFavorite = yield 'java'; if (isFavorite) { /** * Обратите внимание на звёздочку * * Данная строка то же самое, что и: * yield 'kotlin' * yield 'scala' * yield 'closure' * */ yield* jvmLangs() } else { yield 'js'; } yield 'rust';}
function* jvmLangs() {
yield 'kotlin'
yield 'scala'
yield 'closure'
}
function* getLangs() {
const isFavorite = yield 'java';
if (isFavorite) {
/**
* Обратите внимание на звёздочку
*
* Данная строка то же самое, что и:
* yield 'kotlin'
* yield 'scala'
* yield 'closure'
*
*/
yield* jvmLangs()
} else {
yield 'js';
}
yield 'rust';
}
Мы как будто разворачиваем одну книгу внутри другой, и продолжаем читать текст из этой развёрнутой книги.
const generator = getLangs()generator.next()// { value: 'java', done: false }generator.next(true)// { value: 'kotlin', done: false }generator.next()// { value: 'scala', done: false }generator.next()// { value: 'closure', done: false }generator.next()// { value: 'rust', done: false }
const generator = getLangs()
generator.next()
// { value: 'java', done: false }
generator.next(true)
// { value: 'kotlin', done: false }
generator.next()
// { value: 'scala', done: false }
generator.next()
// { value: 'closure', done: false }
generator.next()
// { value: 'rust', done: false }
Так можно вызывать генераторы внутри генераторов и удобно разбивать логику на отдельные части.
Генератор vs. Итератор
СкопированоОбъект-генератор является расширенной версией объекта-итератора, поэтому его также можно использовать для создания коллекций, например, Array или Set.
function* nums() { yield 1 yield 2 yield 3}const arr = Array.from(nums())console.log(arr)// [1, 2, 3]const set = new Set(nums())console.log(set)// Set { 1, 2, 3 }
function* nums() {
yield 1
yield 2
yield 3
}
const arr = Array.from(nums())
console.log(arr)
// [1, 2, 3]
const set = new Set(nums())
console.log(set)
// Set { 1, 2, 3 }
Помимо next, у объекта-генератора есть методы return и throw, которые завершают генератор после их вызова.
При наличии оператора return или после вызова метода return с любым аргументом, в поле value будет находиться указанное значение.
function* generator() { yield 1 yield 2 return 3}for (const num of generator()) { console.log(num)}// 1// 2
function* generator() {
yield 1
yield 2
return 3
}
for (const num of generator()) {
console.log(num)
}
// 1
// 2
Вызов return с переданным аргументом:
function* getLangs() { yield 'java'; yield 'js'; yield 'rust';}const generator = getLangs()generator.next()// { value: 'java', done: false }generator.return('Programming is too hard!')// { value: 'Programming is too hard!', done: true }generator.next()// { value: undefined, done: true }
function* getLangs() {
yield 'java';
yield 'js';
yield 'rust';
}
const generator = getLangs()
generator.next()
// { value: 'java', done: false }
generator.return('Programming is too hard!')
// { value: 'Programming is too hard!', done: true }
generator.next()
// { value: undefined, done: true }
Метод throw позволяет бросить ошибку и завершить генератор.
function* getLangs() { try { yield 'java'; yield 'js'; yield 'rust'; } catch (e) { console.log(e) }}const generator = getLangs()generator.next()// { value: 'java', done: false }generator.throw(new Error('Too much OOP. Brain is melted'))// Error: Too much OOP. Brain is meltedgenerator.next()// { value: undefined, done: true }
function* getLangs() {
try {
yield 'java';
yield 'js';
yield 'rust';
} catch (e) {
console.log(e)
}
}
const generator = getLangs()
generator.next()
// { value: 'java', done: false }
generator.throw(new Error('Too much OOP. Brain is melted'))
// Error: Too much OOP. Brain is melted
generator.next()
// { value: undefined, done: true }
Оператор break в цикле тоже завершает генератор, после чего его невозможно использовать повторно в новом цикле.
Итератор остановит перебор, но его можно использовать повторно.
const generator = getLangs()const langs = []for(const lang of generator){ langs.push(lang) if(langs.length === 1) break}console.log(langs.length)// 1// Новый циклfor(const lang of generator){ langs.push(lang) if(langs.length === 2) break}console.log(langs.length)// Всё ещё 1, а ожидалось 2
const generator = getLangs()
const langs = []
for(const lang of generator){
langs.push(lang)
if(langs.length === 1) break
}
console.log(langs.length)
// 1
// Новый цикл
for(const lang of generator){
langs.push(lang)
if(langs.length === 2) break
}
console.log(langs.length)
// Всё ещё 1, а ожидалось 2
Повторим этот же пример с использованием итератора.
function getLangs() { let index = 0 const langs = ['java', 'js', 'rust'] return { [Symbol.iterator](){ return this }, next(){ return { value: langs[index++], done: index >= langs.length } } }}const iterator = getLangs()const langs = []for(const lang of iterator){ langs.push(lang) if(langs.length === 1) break}console.log(langs.length)// 1// Новый циклfor(const lang of iterator){ langs.push(lang) if(langs.length === 2) break}console.log(langs.length)// 2
function getLangs() {
let index = 0
const langs = ['java', 'js', 'rust']
return {
[Symbol.iterator](){
return this
},
next(){
return {
value: langs[index++],
done: index >= langs.length
}
}
}
}
const iterator = getLangs()
const langs = []
for(const lang of iterator){
langs.push(lang)
if(langs.length === 1) break
}
console.log(langs.length)
// 1
// Новый цикл
for(const lang of iterator){
langs.push(lang)
if(langs.length === 2) break
}
console.log(langs.length)
// 2
Если присвоить функцию-генератор в свойство Symbol объекта-генератора, то генератор можно использовать повторно.
const generator = getLangs()// Присвоим функцию-генератор в свойство Symbol.iteratorgenerator[Symbol.iterator] = function*(){ yield 'java'; yield 'js'; yield 'rust';}const langs = []for(const lang of generator){ langs.push(lang) if(langs.length === 1) break}console.log(langs.length)// 1// Новый циклfor(const lang of generator){ langs.push(lang) if(langs.length === 2) break}console.log(langs.length)// 2
const generator = getLangs()
// Присвоим функцию-генератор в свойство Symbol.iterator
generator[Symbol.iterator] = function*(){
yield 'java';
yield 'js';
yield 'rust';
}
const langs = []
for(const lang of generator){
langs.push(lang)
if(langs.length === 1) break
}
console.log(langs.length)
// 1
// Новый цикл
for(const lang of generator){
langs.push(lang)
if(langs.length === 2) break
}
console.log(langs.length)
// 2