Генераторы появились вместе со стандартом ES6 в 2015 году, и к этому времени уже поддерживаются всеми современными браузерами. В документации об итераторах и генераторах на MDN написано вроде бы и подробно, но не очень понятно с точки зрения именно практического применения этих языковых конструкций.
Генераторы (Generators)— это особый тип функций, которые могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени, что позволяет запускать другой код в момент приостановления функции. Для объявления генератора используется синтаксическая конструкция: function*
(функция со звёздочкой).
Для чего нужны генераторы?
Генераторы нужны для:
- Производства ряда значений по требованию, например, бесконечных последовательностей типа чисел Фибоначчи
- Добавления функционала итерабельности любому объекту с помощью метода Symbol.iterator
- Выполнения асинхронных операций с
acync ... await
Объявление генератора
Генераторы - это обычные функции, которые объявляются с использованием знака *
и возвращают только одно-единственное значение (или ничего). Внутри функции-генератора используется ключевое слово yield
, которое позволяет получать значения с помощью функции next()
.
При вызове функции-генератор возвращается специальный объект, так называемый «генератор», для управления её выполнением. При вызове он запускает выполнение кода до ближайшей инструкции yield <значение>
. По достижении yield
выполнение функции приостанавливается, а соответствующее значение – возвращается во внешний код.
Генераторы отлично работают с итерируемыми (перебираемыми) объектами. В этом случае функция next()
, как правило, автоматически вызывается в методе Symbol.iterator()
для цикличного повторения неких действий, например в цикле for...of
.
Для объявления генератора используется специальная синтаксическая конструкция: function*
, которая так и называется - "функция-генератор".
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // обычная функция - Function Declaration function* myGenerator() { ... } function * myGenerator() { ... } function *myGenerator() { ... } // Function Expression const myGenerator = function* () { ... } // в качестве функции(метода) литерала объекта const myObj = { name: 'generator', *myGenerator() { ... } *[Symbol.iterator](){ ... } } // в классе в качестве метода class ClassName { *myGenerator() { ... } } |
Нельзя объявить генератор с помощью стрелочной функции, будет ошибка:
1 2 | const nextGenerator = *() => { ... } // Uncaught SyntaxError: unexpected token '*' |
Если внимательно присмотреться ко всем вариантам объявления генераторов, то вывод такой: это обычная функция перед именем которой стоит *
Метод next() генератора
Основным методом генератора является next()
. Работа с ним состоит из таких частей:
- Вызов функции-генератора мы записываем в переменную, а затем для нее вызываем метод
next()
. - Метод
next()
находит в функции-генератореyield
и возвращает нам объект в виде{value: somevalue, done: false}
. И так будет продолжаться до тех пор при вызовеnext()
, покаdone
не будет равноtrue
. - При вызове
yield
как бы говорит — передаёмvalue
и ставим паузу, пока не произойдёт следующий вызовnext()
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function* helloGenerator() { yield 'Hello!'; yield 'Hola'; yield 'Привет!'; yield 'Вiтаю!'; yield 'Hallo!'; yield 'Hej!'; yield 'Salut!'; } var helloGen = helloGenerator(); console.log(helloGen.next()); // {value: 'Hello!', done: false} console.log(helloGen.next()); // {value: 'Hola', done: false} console.log(helloGen.next()); // {value: 'Привет!', done: false} console.log(helloGen.next()); // {value: 'Вiтаю!', done: false} |
Каждый раз при вызове метода next()
по достижении yield
выполнение функции приостанавливается, а соответствующее значение – возвращается во внешний код. Внутри генератора можно использовать цикл для генерации любого количества проходов.
1 2 3 4 5 6 7 8 9 10 11 12 13 | function* evenNumbers() { let i = 0; while (true) { yield i += 2; } } var numGen = evenNumbers(); console.log(numGen.next().value); // 2 console.log(numGen.next().value); // 4 console.log(numGen.next().value); // 6 console.log(numGen.next().value); // 8 |
Используем этот подход для того, чтобы сделать сетку случайных цветов. Нажмите внизу на кнопку "Rerun" для обновления примера.
See the Pen Random Colors JS Generator by Elen (@ambassador) on CodePen.
Если вызвать функцию из этого кода со спред-оператором, то мы получим набор или массив цветовых кодов, например:
1 2 3 | let someColorsGen = colorGenerator(6); console.log(...someColorsGen);// #552098 #d98671 #861b95 #06cf92 #8a954f #028eb6 console.log([...someColorsGen]); //['#c1d4df', '#204947', '#6879b6', '#732d56', '#9035c3', '#f05d5d'] |
Еще один пример генератора - для создания счетчика обратного отсчета.
See the Pen Countdown by Elen (@ambassador)on CodePen.
Создание бесконечных последовательностей с помощью генераторов
Генераторы также могут использоваться для создания бесконечных последовательностей значений без необходимости хранения их всех в памяти. Самый простой пример - это генерация последовательных чисел - что-то вроде счетчика, который "держит в уме" функция-генератор:
1 2 3 4 5 6 7 8 9 10 11 | function* infiniteSequence() { let i = 0; while (true) { yield i++; } } const sequence = infiniteSequence(); console.log(sequence.next().value); // 0 console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 2 |
И пример посложнее с одной известной последовательностью чисел - в следующем примере возвращается самовызывающаяся функция (IIFE), которая при клике на кнопку выводит следующее число последовательности Фибоначчи.
See the Pen Fibonacci - JavaScript Generators by Elen (@ambassador) on CodePen.
Добавляем итерабельность в объект
Мы можем добавлять итераторы для неитерируемых, т.е. неперебираемых объектов. Это могут быть объекты какого-либо пользовательского класса. Для того чтобы перебирать эти объекты, мы добавляем к ним метод [Symbol.iterator]
и задаем его вызов в виде функции-генератора.
В примере ниже генератор *[Symbol.iterator]()
создает итератор для перебора символов от 'A' до 'K' включительно. Мы используем метод String.fromCharCode()
для преобразования числового кода символа в сам символ. Вы можете поменять начальный и конечный символ в коде самостоятельно. Например, строка const letterGenerator = new LetterRange('A', 'я')
приведет уже к выводу значительно бОльшого количества символов.
See the Pen Generator in [Symbol.iterator] by Elen (@ambassador) on CodePen.
Пример использования генераторов для эффекта печатной машинки
See the Pen Typing effect using JavaScript - Day 10 of #30Days30Projects by Elen (@ambassador) on CodePen.
Асинхронные генераторы
В мире современного веб-разработчика асинхронность играет ключевую роль. Давайте посмотрим, как асинхронные генераторы в JavaScript предоставляют элегантное решение для эффективного управления асинхронными операциями.
Генераторы могут быть мощным средством управления асинхронным кодом, особенно с появлением ключевого слова yield
. Это позволяет остановить выполнение функции до тех пор, пока не будет завершена асинхронная задача. В этом случае в генераторе будут использоваться промисы.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function* asyncGenerator() { const result1 = yield fetchData('url1'); const result2 = yield fetchData('url2'); console.log(result1, result2); } function fetchData(url) { return new Promise((resolve) => { setTimeout(() => resolve(`Data from ${url}`), 1000); }); } const generator = asyncGenerator(); const promise1 = generator.next().value; promise1.then((result) => generator.next(result).value) .then((result) => generator.next(result)); |
Параллельные асинхронные запросы
Асинхронные генераторы позволяют делать параллельные асинхронные запросы, что улучшает производительность и сокращает время выполнения.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | async function fetchData(url) { return new Promise((resolve) => { setTimeout(() => resolve(`Data from ${url}`), 1000); }); } async function* parallelAsyncGenerator() { const resultPromises = [ fetchData('url1'), fetchData('url2'), fetchData('url3') ]; const results = await Promise.all(resultPromises); yield results; } const generator = parallelAsyncGenerator(); generator.next().then(({ value, done }) => { console.log(value); // [ 'Data from url1', 'Data from url2', 'Data from url3' ] }); |
Обработка ошибок в асинхронных генераторах
Асинхронные генераторы также облегчают обработку ошибок в асинхронном коде, что делает их удобным инструментом для управления ошибками в асинхронных операциях.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | async function fetchDataWithError(url) { return new Promise((resolve, reject) => { const random = Math.random(); setTimeout(() => { if (random < 0.8) { resolve(`Data from ${url}`); } else { reject(new Error(`Failed to fetch data from ${url}`)); } }, 1000); }); } async function* errorHandlingAsyncGenerator() { try { const result1 = await fetchDataWithError('url1'); yield result1; const result2 = await fetchDataWithError('url2'); yield result2; } catch (error) { console.error(error.message); } } const generator = errorHandlingAsyncGenerator(); generator.next().then(({ value, done }) => { console.log(value); // Data from url1 return generator.next(); }).then(({ value, done }) => { console.log(value); // Failed to fetch data from url2 }); |
В примере ниже один асинхронный генератор получает с ресурса JSONPlaceholder сначала имена пользователей и создает ссылку на загрузку постов. Т.е. при клике на имя пользователя, функция displayUserPosts
вызывается с userId
, и она загружает и отображает посты этого пользователя в блоке с классом posts
. Кроме того, здесь также обрабатываются возможные ошибки.
See the Pen Untitled by Elen (@ambassador) on CodePen.
Болше интересных примеров вы найдете в статье по ссылке (англ).
Заключение
Генераторы в JavaScript предоставляют разработчикам мощный инструмент для улучшения потока управления, управления асинхронностью и создания эффективных итерируемых структур данных. Используя их с умом, вы можете значительно улучшить читаемость, поддерживаемость и эффективность вашего кода.