О генераторах в JavaScript ES6, и о том, почему изучать их необязательно
С ростом популярности конструкции async/await растёт и интерес к её внутренним механизмам. Порывшись в интернете, несложно выяснить, что в основе async/await лежат широко известные промисы, и генераторы, которые пользуются куда меньшей известностью и популярностью.

Материал, перевод которого мы сегодня публикуем, посвящён генераторам. А именно, тут мы поговорим о том, как они работают, и о том, как они, совместно с промисами, используются в недрах конструкции async/await. Автор этой статьи говорит, что генераторы, ради их практического применения, осваивать необязательно. Кроме того, он отмечает, что он рассчитывает на то, что читатель немного разбирается в промисах.
Итераторы и генераторы
В JavaScript, начиная с выхода стандарта ES6, появилось несколько новых возможностей, которые направлены на упрощение работы с асинхронными потоками данных и коллекциями. В эту категорию попадают итераторы и генераторы.
Примечательной возможностью итераторов является то, что они предоставляют средства для доступа к элементам коллекций по одному за раз, и при этом позволяют отслеживать идентификатор текущего элемента.
function makeIterator(array) < var nextIndex = 0; console.log("nextIndex =>", nextIndex); return < next: function() < return nextIndex < array.length ? < value: array[nextIndex++], done: false >: < done: true >; > >; > var it = makeIterator(["simple", "iterator"]); console.log(it.next()); // console.log(it.next()); // console.log(it.next()); //
Выше мы передаём функции makeIterator() небольшой массив, содержащий пару элементов, после чего проходимся по нему с помощью итератора, вызывая метод it.next() . Обратите внимание на комментарии, демонстрирующие получаемые с помощью итератора результаты.
Теперь поговорим о генераторах. Генераторы — это функции, которые работают как фабрики итераторов. Рассмотрим простой пример, а затем поговорим о двух механизмах, имеющих отношение к генераторам.
function* sample() < yield "simple"; yield "generator"; >var it = sample(); console.log(it.next()); // console.log(it.next()); // console.log(it.next()); //
Обратите внимание на звёздочку в объявлении функции. Это указывает на то, что данная функция является генератором. Кроме того, взгляните на ключевое слово yield . Оно приостанавливает выполнение функции и возвращает некое значение. Собственно, эти две особенности и являются теми самыми двумя механизмами, о которых мы говорили выше:
- Функция-генератор — это функция, объявленная с использованием звёздочки около ключевого слова function или около имени функции.
- Итератор генератора создаётся, когда вызывают функцию-генератор.
Теперь, когда мы разобрались в основах, поговорим о более интересных вещах. Итераторы и генераторы могут обмениваться данными в двух направлениях. А именно, генераторы, с помощью ключевого слова yield , могут возвращать значения итераторам, однако и итераторы могут отправлять данные генераторам, используя метод iterator.next(‘someValue’) . Вот как это выглядит.
function* favBeer() < const reply = yield "What is your favorite type of beer?"; console.log(reply); if (reply !== "ipa") return "No soup for you!"; return "OK, soup."; > < const it = favBeer(); const q = it.next().value; // Итератор задаёт вопрос console.log(q); const a = it.next("lager").value; // Получен ответ на вопрос console.log(a); >// What is your favorite beer? // lager // No soup for you! < const it = favBeer(); const q = it.next().value; // Итератор задаёт вопрос console.log(q); const a = it.next("ipa").value; // получен ответ на вопрос console.log(a); >// What is your favorite been? // ipa // OK, soup.
Генераторы и промисы
Теперь мы можем поговорить о том, как генераторы и промисы формируют базу конструкции async/await. Представьте, что вместо того, чтобы возвращать, с помощью ключевого слова yield , некие значения, генератор возвращает промисы. При таком раскладе генератор можно обернуть в функцию, которая будет ожидать разрешения промиса и возвращать значение промиса генератору в методе .next() , как было показано в предыдущем примере. Существует популярная библиотека, co, которая выполняет именно такие действия. Выглядит это так:
co(function* doStuff()< var result - yield someAsyncMethod(); var another = yield anotherAsyncFunction(); >);
Итоги
По мнению автора этого материала JS-разработчикам нужно знать о том, как работают генераторы, лишь для того, чтобы понимать особенности внутреннего устройства конструкции async/await. А вот использовать их непосредственно в собственном коде не стоит. Генераторы вводят в JavaScript возможность приостанавливать выполнение функции и возвращаться к ней когда (и если) разработчик сочтёт это необходимым. До сих пор мы, работая с JS-функциями, ожидали, что они, будучи вызванными, просто выполняются от начала до конца. Возможность их приостанавливать — это уже что-то новое, но этот функционал удобно реализован в конструкции async/await.
С этим мнением, конечно, можно и поспорить. Например, один из аргументов в пользу генераторов, сводится к тому, что знание того, как они работают, полезно для отладки кода с async/await, так как внутри этой конструкции скрываются генераторы. Однако автор материала полагает, что это, всё же, нечто иное, нежели использование генераторов в собственном коде.
Уважаемые читатели! Что вы думаете о генераторах? Может быть, вы знаете какие-то варианты их использования, которые оправдывают их непосредственное применение в коде JS-проектов?
Царский промо-код для скидки в 10% на наши виртуальные сервера:
Генераторы
Обычные функции возвращают только одно-единственное значение (или ничего).
Генераторы могут порождать (yield) множество значений одно за другим, по мере необходимости. Генераторы отлично работают с перебираемыми объектами и позволяют легко создавать потоки данных.
Функция-генератор
Для объявления генератора используется специальная синтаксическая конструкция: function* , которая называется «функция-генератор».
Выглядит она так:
function* generateSequence()
Функции-генераторы ведут себя не так, как обычные. Когда такая функция вызвана, она не выполняет свой код. Вместо этого она возвращает специальный объект, так называемый «генератор», для управления её выполнением.
function* generateSequence() < yield 1; yield 2; return 3; >// "функция-генератор" создаёт объект "генератор" let generator = generateSequence(); alert(generator); // [object Generator]
Выполнение кода функции ещё не началось:
Основным методом генератора является next() . При вызове он запускает выполнение кода до ближайшей инструкции yield (значение может отсутствовать, в этом случае оно предполагается равным undefined ). По достижении yield выполнение функции приостанавливается, а соответствующее значение – возвращается во внешний код:
Результатом метода next() всегда является объект с двумя свойствами:
- value : значение из yield .
- done : true , если выполнение функции завершено, иначе false .
Например, здесь мы создаём генератор и получаем первое из возвращаемых им значений:
function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); let one = generator.next(); alert(JSON.stringify(one)); //
На данный момент мы получили только первое значение, выполнение функции остановлено на второй строке:
Повторный вызов generator.next() возобновит выполнение кода и вернёт результат следующего yield :
let two = generator.next(); alert(JSON.stringify(two)); //
И, наконец, последний вызов завершит выполнение функции и вернёт результат return :
let three = generator.next(); alert(JSON.stringify(three)); //
Сейчас генератор полностью выполнен. Мы можем увидеть это по свойству done:true и обработать value:3 как окончательный результат.
Новые вызовы generator.next() больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: .
function* f(…) или function *f(…) ?
Нет разницы, оба синтаксиса корректны.
Но обычно предпочтителен первый вариант, так как звёздочка относится к типу объявляемой сущности ( function* – «функция-генератор»), а не к её названию, так что резонно расположить её у слова function .
Перебор генераторов
Как вы, наверное, уже догадались по наличию метода next() , генераторы являются перебираемыми объектами.
Возвращаемые ими значения можно перебирать через for..of :
function* generateSequence() < yield 1; yield 2; return 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2 >
Выглядит гораздо красивее, чем использование .next().value , верно?
…Но обратите внимание: пример выше выводит значение 1 , затем 2 . Значение 3 выведено не будет!
Это из-за того, что перебор через for..of игнорирует последнее значение, при котором done: true . Поэтому, если мы хотим, чтобы были все значения при переборе через for..of , то надо возвращать их через yield :
function* generateSequence() < yield 1; yield 2; yield 3; >let generator = generateSequence(); for(let value of generator) < alert(value); // 1, затем 2, затем 3 >
Так как генераторы являются перебираемыми объектами, мы можем использовать всю связанную с ними функциональность, например оператор расширения . :
function* generateSequence() < yield 1; yield 2; yield 3; >let sequence = [0, . generateSequence()]; alert(sequence); // 0, 1, 2, 3
В коде выше . generateSequence() превращает перебираемый объект-генератор в массив элементов (подробнее ознакомиться с оператором расширения можно в главе Остаточные параметры и оператор расширения)
Использование генераторов для перебираемых объектов
Некоторое время назад, в главе Перебираемые объекты, мы создали перебираемый объект range , который возвращает значения from..to .
Давайте вспомним код:
let range = < from: 1, to: 5, // for..of range вызывает этот метод один раз в самом начале [Symbol.iterator]() < // . он возвращает перебираемый объект: // далее for..of работает только с этим объектом, запрашивая следующие значения return < current: this.from, last: this.to, // next() вызывается при каждой итерации цикла for..of next() < // нужно вернуть значение как объект if (this.current ; > else < return < done: true >; > > >; > >; // при переборе объекта range будут выведены числа от range.from до range.to alert([. range]); // 1,2,3,4,5
Мы можем использовать функцию-генератор для итерации, указав её в Symbol.iterator .
Вот тот же range , но с гораздо более компактным итератором:
let range = < from: 1, to: 5, *[Symbol.iterator]() < // краткая запись для [Symbol.iterator]: function*() for(let value = this.from; value > >; alert( [. range] ); // 1,2,3,4,5
Это работает, потому что range[Symbol.iterator]() теперь возвращает генератор, и его методы – в точности то, что ожидает for..of :
- у него есть метод .next()
- который возвращает значения в виде
Это не совпадение, конечно. Генераторы были добавлены в язык JavaScript, в частности, с целью упростить создание перебираемых объектов.
Вариант с генератором намного короче, чем исходный вариант перебираемого range , и сохраняет те же функциональные возможности.
Генераторы могут генерировать бесконечно
В примерах выше мы генерировали конечные последовательности, но мы также можем сделать генератор, который будет возвращать значения бесконечно. Например, бесконечная последовательность псевдослучайных чисел.
Конечно, нам потребуется break (или return ) в цикле for..of по такому генератору, иначе цикл будет продолжаться бесконечно, и скрипт «зависнет».
Композиция генераторов
Композиция генераторов – это особенная возможность генераторов, которая позволяет прозрачно «встраивать» генераторы друг в друга.
Например, у нас есть функция для генерации последовательности чисел:
function* generateSequence(start, end)
Мы хотели бы использовать её при генерации более сложной последовательности:
- сначала цифры 0..9 (с кодами символов 48…57)
- за которыми следуют буквы в верхнем регистре A..Z (коды символов 65…90)
- за которыми следуют буквы алфавита a..z (коды символов 97…122)
Мы можем использовать такую последовательность для генерации паролей, выбирать символы из неё (может быть, ещё добавить символы пунктуации), но сначала её нужно сгенерировать.
В обычной функции, чтобы объединить результаты из нескольких других функций, мы вызываем их, сохраняем промежуточные результаты, а затем в конце их объединяем.
Для генераторов есть особый синтаксис yield* , который позволяет «вкладывать» генераторы один в другой (осуществлять их композицию).
Вот генератор с композицией:
function* generateSequence(start, end) < for (let i = start; i function* generatePasswordCodes() < // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); >let str = ''; for(let code of generatePasswordCodes()) < str += String.fromCharCode(code); >alert(str); // 0..9A..Za..z
Директива yield* делегирует выполнение другому генератору. Этот термин означает, что yield* gen перебирает генератор gen и прозрачно направляет его вывод наружу. Как если бы значения были сгенерированы внешним генератором.
Результат – такой же, как если бы мы встроили код из вложенных генераторов:
function* generateSequence(start, end) < for (let i = start; i function* generateAlphaNum() < // yield* generateSequence(48, 57); for (let i = 48; i let str = ''; for(let code of generateAlphaNum()) < str += String.fromCharCode(code); >alert(str); // 0..9a..zA..Z
Композиция генераторов – естественный способ вставлять вывод одного генератора в поток другого. Она не использует дополнительную память для хранения промежуточных результатов.
yield – дорога в обе стороны
До этого момента генераторы сильно напоминали перебираемые объекты, со специальным синтаксисом для генерации значений. Но на самом деле они намного мощнее и гибче.
Всё дело в том, что yield – дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор.
Чтобы это сделать, нам нужно вызвать generator.next(arg) с аргументом. Этот аргумент становится результатом yield .
Продемонстрируем это на примере:
function* gen() < // Передаём вопрос во внешний код и ожидаем ответа let result = yield "2 + 2 = ?"; // (*) alert(result); >let generator = gen(); let question = generator.next().value; // передаём результат в генератор
- Первый вызов generator.next() – всегда без аргумента, он начинает выполнение и возвращает результат первого yield «2+2=?» . На этой точке генератор приостанавливает выполнение.
- Затем, как показано на картинке выше, результат yield переходит во внешний код в переменную question .
- При generator.next(4) выполнение генератора возобновляется, а 4 выходит из присваивания как результат: let result = 4 .
Обратите внимание, что внешний код не обязан немедленно вызывать next(4) . Ему может потребоваться время. Это не проблема, генератор подождёт.
// возобновить генератор через некоторое время setTimeout(() => generator.next(4), 1000);
Как видно, в отличие от обычных функций, генератор может обмениваться результатами с вызывающим кодом, передавая значения в next/yield .
Чтобы сделать происходящее более очевидным, вот ещё один пример с большим количеством вызовов:
function* gen() < let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 >let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true
- Первый .next() начинает выполнение… Оно доходит до первого yield .
- Результат возвращается во внешний код.
- Второй .next(4) передаёт 4 обратно в генератор как результат первого yield и возобновляет выполнение.
- …Оно доходит до второго yield , который станет результатом .next(4) .
- Третий next(9) передаёт 9 в генератор как результат второго yield и возобновляет выполнение, которое завершается окончанием функции, так что done: true .
Получается такой «пинг-понг»: каждый next(value) передаёт в генератор значение, которое становится результатом текущего yield , возобновляет выполнение и получает выражение из следующего yield .
generator.throw
Как мы видели в примерах выше, внешний код может передавать значение в генератор как результат yield .
…Но можно передать не только результат, но и инициировать ошибку. Это естественно, так как ошибка является своего рода результатом.
Для того, чтобы передать ошибку в yield , нам нужно вызвать generator.throw(err) . В таком случае исключение err возникнет на строке с yield .
Например, здесь yield «2 + 2 = ?» приведёт к ошибке:
function* gen() < try < let result = yield "2 + 2 = ?"; // (1) alert("Выполнение программы не дойдёт до этой строки, потому что выше возникнет исключение"); >catch(e) < alert(e); // покажет ошибку >> let generator = gen(); let question = generator.next().value; generator.throw(new Error("Ответ не найден в моей базе данных")); // (2)
Ошибка, которая проброшена в генератор на строке (2) , приводит к исключению на строке (1) с yield . В примере выше try..catch перехватывает её и отображает.
Если мы не хотим перехватывать её, то она, как и любое обычное исключение, «вывалится» из генератора во внешний код.
Текущая строка вызывающего кода – это строка с generator.throw , отмечена (2) . Таким образом, мы можем отловить её во внешнем коде, как здесь:
function* generate() < let result = yield "2 + 2 = ?"; // Ошибка в этой строке >let generator = generate(); let question = generator.next().value; try < generator.throw(new Error("Ответ не найден в моей базе данных")); >catch(e) < alert(e); // покажет ошибку >
Если же ошибка и там не перехвачена, то дальше – как обычно, она выпадает наружу и, если не перехвачена, «повалит» скрипт.
Итого
- Генераторы создаются при помощи функций-генераторов function* f(…) .
- Внутри генераторов и только внутри них существует оператор yield .
- Внешний код и генератор обмениваются промежуточными результатами посредством вызовов next/yield .
В современном JavaScript генераторы используются редко. Но иногда они оказываются полезными, потому что способность функции обмениваться данными с вызывающим кодом во время выполнения совершенно уникальна. И, конечно, для создания перебираемых объектов.
Также, в следующей главе мы будем изучать асинхронные генераторы, которые используются, чтобы читать потоки асинхронно сгенерированных данных (например, постранично загружаемые из сети) в цикле for await . of .
В веб-программировании мы часто работаем с потоками данных, так что это ещё один важный случай использования.
Задачи
Псевдослучайный генератор
Есть много областей, где нам нужны случайные данные.
Одной из них является тестирование. Нам могут понадобиться случайные данные: текст, числа и т.д., чтобы хорошо всё проверить.
В JavaScript мы можем использовать Math.random() . Но если что-то пойдёт не так, то нам нужно будет перезапустить тест, используя те же самые данные.
Для этого используются так называемые «сеяные псевдослучайные генераторы». Они получают «зерно», как первое значение, и затем генерируют следующее, используя формулу. Так что одно и то же зерно даёт одинаковую последовательность, и, следовательно, весь поток легко воспроизводим. Нам нужно только запомнить зерно, чтобы воспроизвести последовательность.
Пример такой формулы, которая генерирует более-менее равномерно распределённые значения:
next = previous * 16807 % 2147483647
Если мы используем 1 как зерно, то значения будут:
- 16807
- 282475249
- 1622650073
- …и так далее…
Задачей является создать функцию-генератор pseudoRandom(seed) , которая получает seed и создаёт генератор с указанной формулой.
let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073
Генераторы в JS — что это, зачем это и почему это красиво
Долгое время генераторы оставались для меня такой странной фичей JS, которая, казалось мне, нужна только для того, чтобы спрашивать о них на собеседованиях (я так не делал, но, когда интервьюировали меня, о генераторах спрашивали довольно часто). Но не так давно я понял, насколько же это важная и интересная штука.
Немного о том, как работать с генераторами
Генераторы похожи на функции, но значительно от них отличаются. Обычные функции возвращают либо что-то одно, либо ничего. Генераторы же могут, приостанавливая свое выполнение, вернуть неограниченное количество значений. Это очень пересекается с понятием “монада”, о котором вы можете почитать в моей статье о монадах. Более того, генераторы решают кучу проблем, связанных с монадами — об этом я расскажу чуть позже.
А пока — давайте посмотрим на пример:
function* generateNumberSequence() yield 1; yield 2; yield 3; yield 4; yield 5; yield 6; yield 7; yield 8; yield 9; return 10; >
Функцию-генератор мы объявляем именно так, function* , со звездочкой. Когда мы выполняем такую функцию, мы получаем сам генератор:
const generator = generateNumberSequence();
У генератора есть метод next() , который выполняет все, пока не встретит ключевое слово yield . Тогда он вернет значение из yield (удобно представлять себе yield как неокончательный return ) или undefined , если такого значения нет. После этого генератор приостановит выполнение и будет ждать следующего вызова next() .
Результатом вызова next() всегда становится такой объект:
value, // значение из yield done // true, если генератор полностью выполнен, в ином случае false >
Давайте поглядим на пример целиком:
function* generateNumberSequence() yield 1; yield 2; yield 3; yield 4; yield 5; yield 6; yield 7; yield 8; yield 9; return 10; > const generator = generateNumberSequence(); const one = generator.next(); // const two = generator.next(); // // и так далее, до: const ten = generator.next() // // по значению done: true мы можем понять, что генератор полностью выполнен. // мы можем и дальше запускать next() неограниченное количество раз, но это бесполезно: const oneMoreTime = generator.next() // const andLastTime = generator.next() //
Генератор как Iterable (перебор генераторов)
Достаточно очевидно, что из-за наличия метода next() генераторы — это перебираемые объекты. Это значит, что мы можем делать так:
const generator = generateNumberSequence(); for (const value of generator) console.log(value) > /* 1 2 3 4 5 6 7 8 9 */
Выглядит приятнее, да? Но есть момент: return в таком случае не выполнится, и мы не увидим значения 10 . Поэтому можно просто не использовать return в генераторах, ограничившись yield .
Еще момент: генераторы не обязательно конечны. Мы можем написать и бесконечный генератор:
function* infiniteGenerator() for (let i = 0; i 10; i--) yield i; > > // если и использовать такие генераторы, то нужно не забывать про break / return
Композиция генераторов
В случае с обычными функциями, чтобы их объединить, мы отдельно исполняем их, сохраняя промежуточные результаты и объединяя их в конце. А у генераторов есть очень интересная возможность, в отличие от обычных функций: мы можем встраивать генераторы друг в друга, используя синтаксис yield* .
Смотрите, что можно сделать:
// генератор последовательностей от start до end function* generateNumbers(start, end) for (let i = start; i end; i++) yield i; > > function* sequenceGenerator() yield* generateNumbers(1, 100); yield* generateNumbers(200, 300); > for (const num of sequenceGenerator()) console.log(num); > // 1 .. 100 200 .. 300
Таким образом, не используя дополнительную память для сохранения промежуточных результатов и не разрабатывая огромный генератор на очень много строк, мы пишем гораздо более оптимальный и читаемый код.
И еще немного интересного об yield
До этого момента мы использовали yield только для того, чтобы что-то отдать из генератора наружу. Хоть это само по себе очень полезно в том смысле, что мы можем собирать очень кастомные Iterable -объекты, но это не все, что yield умеет — еще с помощью него мы можем снаружи передать что-то в генератор, ведь в next() можно передавать аргументы, которые становятся результатом yield . Смотрите:
function* inputGen() let result = yield 'awaiting something from the outside' yield result; yield result.toUpperCase(); > const gen = inputGen(); console.log(gen.next().value); // awaiting something from the outside console.log(gen.next('something').value); // something console.log(gen.next().value); // SOMETHING
Видите? Мы передали что-то в next, в генераторе сохранили это в result и применили к нему .toUpperCase() . Таким образом, генератор — это не просто прокачанный перебираемый объект, это нечто гораздо более функциональное.
Еще один интересный метод генераторов — throw , с помощью которого мы можем выкидывать ошибки из генераторов. Смотрите:
function* inputGen() let result = yield 'awaiting something from the outside' > const gen = inputGen(); const question = gen.next().value; try gen.throw(new Error('Ответ не найден')); > catch (e) console.log(e); // Ответ не найден >
Итоги
Вообще, генераторы в реальном коде используются редко. Но как способ работы с разными монадическими структурами (о монадах — здесь) они крайне полезны. Да и вообще, создавать перебираемые объекты, которые во время выполнения могут обмениваться данными с внешним миром — крутая возможность. В общем, учим генераторы 🙂
Интересный пост?
Вот еще похожие:
- Событийно-ориентированная архитектура и Node.js Events
- Реактивное программирование: теория и практика
- Как и зачем писать тесты?
- Функциональное программирование. Что это и зачем?
- Профилирование Node.js-приложений
ES6 в деталях: генераторы
Мне не терпится вам всё рассказать. Сегодня мы будем обсуждать самую волшебную функциональность в ES6.
Что я имел в виду под словом «волшебную»? Во-первых, эта функциональность настолько отличается от всего того, что уже есть в JS, что поначалу может показаться колдовством. В том смысле, что она выворачивает обычное поведение языка наизнанку! Если это не магия, то я не знаю, что это.
Но не только поэтому. Возможности этой фичи по упрощению кода и устранению «ада колбеков» граничат со сверхъестественным.
Я излишне нахваливаю? Давайте углубимся, и вы сами рассудите.
Знакомьтесь, генераторы ES6
Что такое генераторы?
Начнём с рассмотрения одного генератора:
function* quips(name) < yield "привет, " + name + "!"; yield "я надеюсь, вам нравятся статьи"; if (name.startsWith("X")) < yield "как круто, что ваше имя начинается с X, " + name; > yield "увидимся!"; >
Это часть кода для говорящей кошки, возможно, самого важного вида приложений в интернете на сегодняшний день. (Давайте, нажмите на ссылку, поиграйте с кошкой. Когда вы окончательно запутаетесь, возвращайтесь сюда за объяснением.)
Выглядит как-то похоже на функцию, верно? Это называется, функция-генератор, и у неё есть много общего с обычными функциями. Но вы можете заметить два отличия уже сейчас:
- Обычные функции начинаются с function . Функции-генераторы начинаются с function* .
- Внутри функции-генератора есть ключевое слово yield с синтаксисом, похожим на return . Отличие в том, что функция (в том числе функция-генератор) может вернуть значение только один раз, но отдать значение функция-генератор может любое количество раз. Выражение yield приостанавливает выполнение генератора, так что его можно позже возобновить.
Вот, именно в этом самая большая разница между обычными функциями и функциями-генераторами. Обычные функции не могут поставить себя на паузу. Функции-генераторы могут.
Что делают генераторы
Что произойдёт, если запустить функцию-генератор quips() ?
> var iter = quips("jorendorff"); [object Generator] > iter.next() < value: "привет, jorendorff!", done: false > > iter.next() < value: "я надеюсь, вам нравятся статьи", done: false > > iter.next() < value: "увидимся!", done: false > > iter.next() < value: undefined, done: true >
Возможно, вы очень привыкли к обычным функциям и тому, как они себя ведут. Когда их вызывают, они сразу же начинают выполняться и выполняются до тех пор, пока не вернут значение или не бросят исключение. Такое поведение само собой разумеется для любого JS-программиста.
Вызов генератора выглядит так же: quips(«jorendorff») . Но после того, как вы вызовете генератор, он ещё не начнёт выполняться. Вместо этого он вернёт приостановленный объект Generator (в примере выше он под именем iter ). Вы можете считать, что объект Generator — это вызов функции, замороженный во времени. Если точнее, он заморожен прямо в самом начале функции-генератора, перед первой строчкой кода.
Каждый раз, как вы вызываете метод .next() у объекта Generator, вызов функции оттаивает и выполняется, пока не достигнет следующего выражения yield .
Вот почему в примере выше после вызовов iter.next() мы всякий раз получали новое строковое значение. Эти значения производятся выражениями yield в теле quips() .
При последнем вызове iter.next() мы, наконец, достигли конца функции-генератора, так что поле .done результата стало равно true . Добраться до конца функции — это всё равно что вернуть undefined , и именно поэтому поле .value результата равно undefined .
Похоже, сейчас самое время вернуться к странице с говорящей кошкой и как следует поиграться с кодом. Попробуйте добавить yield внутрь цикла. Что произойдёт?
Говоря техническим языком, каждый раз, когда генератор отдаёт значение, его стековый кадр: локальные переменные, аргументы, временные значения и текущая позиция точки выполнения внутри тела генератора — удаляется из стека. Однако, объект Generator хранит ссылку на этот стековый кадр (или его копию), так что последующий вызов .next() возобновит его и продолжит выполнение.
Стоит отметить, что генераторы не являются потоками выполнения. В языках с потоками различные куски кода могут выполняться одновременно, обычно приводя к состояниям гонки, недетерминированности и страстно желанному приросту производительности. Генераторы вообще на это не похожи. Когда генератор выполняется, он работает в том же потоке, что и код его вызвавший. Порядок выполнения последователен и строго определён, и нет никакой параллельности. В отличие от системных потоков, генератор останавливается только на тех местах, где в коде есть yield .
Хорошо. Теперь мы знаем, что такое, генераторы. Мы видели, как генераторы выполняются, приостанавливают и возобновляют свое выполнение. Теперь хороший вопрос: как эти странные возможности могут нам пригодиться?
Генераторы — итераторы
На прошлой неделе мы увидели, что в ES6 итераторы не просто один встроенный класс. Они — точка расширения языка. Вы можете создавать собственные итераторы, и для этого нужно лишь реализовать два метода: [Symbol.iterator]() и .next() .
Но реализация интерфейса — это всегда работа, по меньшей мере, небольшая. Взглянем, как реализация итератора выглядит на практике. В качестве примера возьмём простой итератор range , который всего-навсего считает от одного числа до другого, как в старомодном цикле for (;;) из C.
// Должно "прозвенеть" трижды for (var value of range(0, 3)) < alert("Динь! на этаже № " + value); >
Вот одно решение, с использованием класса ES6. (Если синтаксис class вам не до конца ясен, не волнуйтесь, мы разберём его в одной из будущих статей.)
class RangeIterator < constructor(start, stop) < this.value = start; this.stop = stop; > [Symbol.iterator]() < return this; > next() < var value = this.value; if (value < this.stop) < this.value++; return done: false, value: value>; > else < return done: true, value: undefined>; > > > // Возвращает новый итератор, который считает от 'start' до 'stop'. function range(start, stop) < return new RangeIterator(start, stop); >
Так реализация итератора выглядит в Java или Swift. Неплохо. Но вместе с тем и нетривиально. Есть ли ошибки в этом коде? Трудно сказать. Это выглядит совершенно непохоже на изначальный цикл for (;;) , который мы пытаемся эмулировать: протокол итераторов заставляет нас разобрать этот цикл на части.
В этом месте вы можете слегка охладеть к итераторам. Может, ими и здорово пользоваться, но вот реализовывать их трудно.
Вам, возможно, не пришло бы в голову предлагать добавить новую, пугающую и мозголомную структуру потока выполнения в язык JS просто чтобы стало легче писать итераторы. Но раз уж у нас уже есть генераторы, можем ли мы их тут применить? Давайте попробуем:
function* range(start, stop) < for (var i = start; i < stop; i++) yield i; >
Вот этот генератор из 4 строчек полностью заменяет предыдущую 23-строчную реализацию range() , включая весь класс RangeIterator целиком. Это возможно потому что генераторы — это итераторы. У всех генераторов есть встроенная реализация .next() и [Symbol.iterator]() . Всё, что вам нужно — это описать поведение цикла.
Реализация итераторов без генераторов похожа на случай, когда нужно написать длинное электронное письмо используя только пассивный залог. Когда нельзя просто сказать то, что имеется в виду, речь в итоге получается весьма запутанной. RangeIterator длинный и странный потому что он должен описывать функциональность цикла не используя синтаксис циклов. Генераторы — ответ на это.
Для чего ещё можно применить возможность генераторов вести себя как итераторы?
- Преобразование любого объекта в итерируемый. Просто напишите функцию-генератор, которая перебирает this , отдавая каждое значение по мере работы. Затем установите её объекту как метод [Symbol.iterator] .
- Упрощение функций, создающих массивы. Предположим, у вас есть функция, которая каждый раз при вызове возвращает массив, вроде такой:
// Делим одномерный массив 'icons' // на массивы длиной 'rowLength'. function splitIntoRows(icons, rowLength) < var rows = []; var nRows = Math.ceil(icons.length / rowLength); for (var i = 0; i < icons.length; i += rowLength) < rows.push(icons.slice(i, i + rowLength)); >return rows; >
Генераторы могут немного сократить этот код:
function* splitIntoRows(icons, rowLength) < var nRows = Math.ceil(icons.length / rowLength); for (var i = 0; i < icons.length; i += rowLength) < yield icons.slice(i, i + rowLength); > >
function* filter(test, iterable) < for (var item of iterable) < if (test(item)) yield item; > >
Итак, генераторы полезны? Разумеется. Это удивительно лёгкий способ реализации собственных итераторов, а итераторы — это новый стандарт для данных и циклов во всём ES6.
Но это ещё не всё, что генераторы могут делать. Может даже выясниться, что это не самое важное из того, что они делают.
Генераторы и асинхронный код
Вот такой код JS я писал раньше:
Может быть, вы встретите что-то похожее в своём коде. Асинхронные APIs обычно требуют колбеков, поэтому приходится писать очередную анонимную функцию всякий раз, когда что-то делаешь. И если у вас есть кусок кода, который делает три вещи, вместо трёх строчек кода вы видите три уровня отступов кода.
Вот ещё кое-что из того JS-кода, что я писал:
>).on('close', function ( ) < done(undefined, undefined); >).on('error', function (error) < done(error); >);
В асинхронных API используются соглашения об обработке ошибок вместо исключений. У разных API могут быть разные соглашения. В большинстве из них ошибки просто игнорируются по умолчанию. В некоторых из них игнорируется по умолчанию даже обычное успешное выполнение.
До нынешнего момента эти проблемы были необходимой платой за асинхронное программирование. Мы свыклись с тем, что асинхронный код просто не выглядит так же красиво и просто, как такой же синхронный.
Генераторы дают нам новую надежду, что это так не останется.
Q.async() — это экспериментальная попытка сделать асинхронный код похожим на синхронный при помощи генераторов и промисов. К примеру:
// Синхронный код, производящий шум function makeNoise( ) < shake(); rattle(); roll(); >// Асинхронный код, производящий шум // Возвращает объект Promise, который разрешится, // когда мы закончим шуметь function makeNoise_async( ) < return Q.async(function* ( ) < yield shake_async(); yield rattle_async(); yield roll_async(); >); >
Основное отличие в том, что в асинхронной версии нужно добавлять ключевое слово yield перед любым вызовом асинхронной функции.
Если добавить конструкции вроде if или try / catch в версию Q.async , то всё будет работать точно так же, как если бы их добавили в синхронный код. По сравнению с другими способами написания асинхронного кода этот меньше всего ощущается как изучение нового языка.
Если вы дочитали до этого места, возможно, вам понравится очень подробная статья по этой теме от Джеймса Лонга (James Long).
Итак, генераторы освещают наш путь к новой модели асинхронного программирования, которая, кажется, лучше подходит для человеческого мозга. Эта работа ещё не окончена. Среди всего прочего, может помочь синтаксис получше. Предложение асинхронных функций, работающих на промисах и генераторах и вдохновлённых похожими возможностями в C#, уже внесено в таблицу на включение в ES7.
Когда можно воспользоваться этими безумными вещами?
На сервере вы можете применять генераторы уже сегодня в io.js (или в Node с параметром командной строки —harmony ).
Из браузеров пока что генераторы поддерживают только Firefox 27+ и Chrome 39+. Чтобы применять генераторы в вебе, придётся воспользоваться Babel или Traceur и транслировать код ES6 в понятный всем браузерам ES5.
Ещё кое-что, что нельзя не упомянуть: Генераторы впервые были реализованы в JS Бренданом Айком (Brendan Eich), и его подход очень напоминал генераторы в Python, которые в свою очередь были вдохновлены Icon. Они появились в Firefox в далёком 2006. Путь к стандартизации был непростым, синтаксис и поведение за это время немного поменялись. Генераторы ES6 были реализованы как в Firefox, так и в Chrome мастером компиляции Энди Винго (Andy Wingo). Его работа спонсировалась Bloomberg.
yield;
О генераторах ещё можно многое рассказать. Мы не рассмотрели методы .throw() и .return() , необязательные аргументы .next() и синтаксис выражения yield* . Но я считаю, что эта статья уже достаточно длинная, и из неё и так можно узнать много нового. Как и генераторы, мы пока приостановимся и закончим позднее.
Но на следующей неделе давайте немного сменим тему. Мы охватили две сложные темы подряд. Разве не было бы здорово в следующий раз поговорить о функциональности ES6, которая не изменит вашу жизнь? О чем-нибудь простом и очевидно полезном? О чём-то, что вызовет у вас улыбку? В ES6 и такое есть.
В следующей статье: фича, которая прекрасно подойдёт к любому коду, который вы пишете каждый день. Присоединяйтесь на следующей неделе и мы рассмотрим шаблоны строк в деталях.