Функции-генераторы в JavaScript
Генераторы JavaScript: синтаксис function*, yield и yield*, передача значений через next(), return() и throw(), ленивые бесконечные последовательности.
Генератор — это особый вид функции, которая может приостановить своё выполнение в любой момент, вернуть значение вызывающему коду, а затем продолжить работу ровно с того места, где остановилась. Вместо того чтобы вычислять всё сразу и возвращать единственный результат, генератор производит последовательность значений лениво — по одному, только по запросу.
Эта идея делает генераторы самым элегантным способом в JavaScript создавать пользовательские итерируемые объекты, моделировать бесконечные последовательности без утечек памяти и выполнять длительную логику по требованию. В этом руководстве рассматриваются синтаксис function*, yield и yield*, двусторонняя коммуникация с помощью next(), досрочное завершение и практические паттерны использования генераторов.
Эта страница опирается на материал об итерируемых объектах и итераторах — если протокол итераторов вам ещё незнаком, сначала прочитайте тот раздел.
Понимание функций-генераторов
Основы функций-генераторов
Функция-генератор объявляется как обычная функция, но после ключевого слова function ставится звёздочка: function*. Именно звёздочка отличает генератор от обычной функции.
Вызов функции-генератора не запускает её тело. Вместо этого возвращается объект-генератор — итератор, которым управляют вручную. Каждый вызов метода next() выполняет тело до следующего yield, затем останавливается и возвращает { value, done }:
Первый next() выполняется до первого yield 'Hello' и останавливается там. Второй продолжает выполнение с этой точки и доходит до yield 'World'. Третий возобновляет работу, не находит больше операторов yield, достигает конца функции и возвращает done: true. Последующие вызовы next() продолжат возвращать { value: undefined, done: true }.
Поскольку объект-генератор также является итерируемым, его можно обойти с помощью for...of, оператора расширения или деструктуризации — все они автоматически останавливаются, когда done становится true:
Объект-генератор можно обойти только один раз. После исчерпания for...of не выдаст ничего. Чтобы получить свежий генератор, вызовите функцию-генератор заново.
Возвращаемое значение генератора
Оператор return внутри генератора завершает итерацию и передаёт финальное value вместе с done: true. Обратите внимание, что for...of игнорирует это возвращаемое значение — он видит только значения, переданные через yield:
Управление потоком выполнения с помощью return() и throw()
Помимо next(), объект-генератор предоставляет ещё два метода, которые позволяют вызывающему управлять выполнением извне:
generator.return(value)принудительно завершает генератор немедленно, выполняя все блокиfinallyпо пути, и возвращает{ value, done: true }.generator.throw(error)внедряет исключение в точке текущегоyield, чтобы генератор мог перехватить его черезtry...catch— или пробросить дальше, если не перехватывает.
Блок finally делает генераторы удобным местом для освобождения ресурсов (закрытие файла, отключение потока) даже при досрочном прерывании итерации.
Продвинутые паттерны генераторов
Делегирование с помощью yield*
Когда один генератор должен воспроизвести значения другого генератора или любого итерируемого объекта, используйте yield* (читается «yield-delegate») вместо написания цикла вручную. Он прозрачно пересылает каждое значение из делегированного источника:
Важная деталь: значение выражения yield* равно значению return внутреннего генератора, что позволяет компоновать генераторы и возвращать результат во внешний:
Передача значений в генераторы
Взаимодействие с генератором работает в обе стороны. Выражение yield не только отправляет значение наружу через next(), но и вычисляется в то, что вы передаёте внутрь при следующем вызове next(value). Это превращает генератор в небольшую интерактивную корутину:
Первый next() выполняется до первого yield и возвращает вопрос; его аргумент отбрасывается, поскольку ни один yield ещё не находится в режиме ожидания. Второй next('Alice') возобновляет приостановленный yield, так что выражение принимает значение 'Alice' и присваивается переменной name.
Практическое применение генераторов JavaScript
Создание пользовательских итерируемых объектов
Самое распространённое практическое применение генераторов — сделать собственные объекты итерируемыми. Определите [Symbol.iterator] как метод-генератор, и на объекте заработают for...of, оператор расширения и деструктуризация. Это требует значительно меньше шаблонного кода, чем написание объекта-итератора с ручной реализацией next() (о том, что такое Symbol.iterator, читайте в разделе Тип Symbol):
Ленивые и бесконечные последовательности
Поскольку генератор вычисляет каждое значение только по запросу, он может описывать концептуально бесконечные последовательности, не расходуя память. Классический пример — счётчик, однако тот же подход применяется для диапазонов, генераторов идентификаторов или потоков Фибоначчи. Для работы с такими последовательностями используют вспомогательную функцию «take», которая останавливается после нужного количества элементов:
Параметризованный генератор диапазона — удобный повторно используемый вариант той же идеи:
Управление асинхронными операциями
Исторически генераторы применялись для борьбы с «адом колбэков»: каждый yield приостанавливался на Promise, а внешний «раннер» возобновлял генератор после разрешения этого Promise. Пример ниже демонстрирует ручную механику, чтобы вы понимали, что происходит под капотом:
Это упрощённый ручной раннер. В современном коде вы написали бы это с помощью async/await, который язык построил именно на основе данного паттерна генератор + Promise. Для асинхронной итерации (потоковая передача данных по частям) используйте асинхронные генераторы с async function* и for await...of.
Генераторы и обычные функции
| Аспект | Обычная функция | Функция-генератор |
|---|---|---|
| Объявление | function fn() | function* fn() |
| Вызов | выполняет тело до конца | возвращает объект-генератор, ничего не запускает |
| Возвращает | одно значение | поток значений через yield |
| Может приостановиться? | нет | да, на каждом yield |
| Итерируема? | нет | да (работает с for...of, оператором расширения) |
Используйте генератор, когда нужны значения, производимые лениво или по требованию, когда объект должен быть итерируемым, или когда последовательность может быть бесконечной. Для простого одноразового вычисления обычная функция проще и быстрее.
Заключение
Генераторы дают JavaScript чистый способ производить последовательности лениво, приостанавливать и возобновлять логику, а также делать любые объекты совместимыми с циклами вроде for...of. Освойте yield для выдачи значений, yield* для делегирования другим итерируемым объектам и next()/return()/throw() для управления потоком выполнения — и у вас окажется инструмент, лежащий в основе всего: от пользовательских итерируемых объектов до синтаксиса async/await, построенного на его базе.