Генераторы в 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-приложений
Итераторы и генераторы
Обработка каждого элемента коллекции является весьма распространённой операцией. JavaScript предоставляет несколько способов перебора коллекции, от простого цикла for до map() , filter() и array comprehensions (en-US) . Итераторы и генераторы внедряют концепцию перебора непосредственно в ядро языка и обеспечивают механизм настройки поведения for. of циклов.
Подробнее см. также:
- Iteration protocols
- for. of
- function* и Generator
- yield и yield*
- Generator comprehensions (en-US) Экспериментальная возможность
Итераторы
Объект является итератором, если он умеет обращаться к элементам коллекции по одному за раз, при этом отслеживая своё текущее положение внутри этой последовательности. В JavaScript итератор — это объект, который предоставляет метод next(), возвращающий следующий элемент последовательности. Этот метод возвращает объект с двумя свойствами: done и value.
После создания, объект-итератор может быть явно использован, с помощью вызовов метода next().
function makeIterator(array) var nextIndex = 0; return next: function () return nextIndex array.length ? value: array[nextIndex++], done: false > : done: true >; >, >; >
После инициализации, метод next() может быть вызван для поочерёдного доступа к парам ключ-значение в объекте:
var it = makeIterator(["yo", "ya"]); console.log(it.next().value); // 'yo' console.log(it.next().value); // 'ya' console.log(it.next().done); // true
Генераторы
В то время как пользовательские итераторы могут быть весьма полезны, при их программировании требуется уделять серьёзное внимание поддержке внутреннего состояния. Генераторы предоставляют мощную альтернативу: они позволяют определить алгоритм перебора, написав единственную функцию, которая умеет поддерживать собственное состояние.
Генераторы — это специальный тип функции, который работает как фабрика итераторов. Функция становится генератором, если содержит один или более yield операторов и использует function* синтаксис.
function* idMaker() var index = 0; while (true) yield index++; > var it = idMaker(); console.log(it.next().value); // 0 console.log(it.next().value); // 1 console.log(it.next().value); // 2 // .
Итерируемые объекты
Объект является итерируемым, если в нем определён способ перебора значений, то есть, например, как значения перебираются в конструкции for..of . Некоторые встроенные типы, такие как Array или Map (en-US), по умолчанию являются итерируемыми, в то время как другие типы, как, например, Object , таковыми не являются.
Чтобы быть итерируемым, объект обязан реализовать метод @@iterator, что означает, что он (или один из объектов выше по цепочке прототипов) обязан иметь свойство с именем Symbol.iterator :
Пользовательские итерируемые объекты
Мы можем создать свои собственные итерируемые объекты так:
var myIterable = >; myIterable[Symbol.iterator] = function* () yield 1; yield 2; yield 3; >; [. myIterable]; // [1, 2, 3]
Встроенные итерируемые объекты
Объекты String , Array , TypedArray , Map (en-US) и Set являются итерируемыми, потому что их прототипы содержат метод Symbol.iterator .
Синтаксис для итерируемых объектов
Некоторые выражения работают с итерируемыми объектами, например, for-of циклы, spread operator, yield* , и destructuring assignment.
for (let value of ["a", "b", "c"]) console.log(value); > // "a" // "b" // "c" [. "abc"]; // ["a", "b", "c"] function* gen() yield* ["a", "b", "c"]; > gen().next()[(a, b, c)] = // new Set(["a", "b", "c"]); a; // "a"
Продвинутые генераторы
Генераторы вычисляют результаты своих yield выражений по требованию, что позволяет им эффективно работать с последовательностями с высокой вычислительной сложностью, или даже с бесконечными последовательностями, как продемонстрировано выше.
Метод next() также принимает значение, которое может использоваться для изменения внутреннего состояния генератора. Значение, переданное в next(), будет рассматриваться как результат последнего yield выражения, которое приостановило генератор.
Вот генератор чисел Фибоначчи, использующий next(x) для перезапуска последовательности:
function* fibonacci() var fn1 = 1; var fn2 = 1; while (true) var current = fn2; fn2 = fn1; fn1 = fn1 + current; var reset = yield current; if (reset) fn1 = 1; fn2 = 1; > > > var sequence = fibonacci(); console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 2 console.log(sequence.next().value); // 3 console.log(sequence.next().value); // 5 console.log(sequence.next().value); // 8 console.log(sequence.next().value); // 13 console.log(sequence.next(true).value); // 1 console.log(sequence.next().value); // 1 console.log(sequence.next().value); // 2 console.log(sequence.next().value); // 3
Примечание: Интересно, что вызов next(undefined) равносилен вызову next() . При этом вызов next() для нового генератора с любым аргументом, кроме undefined, спровоцирует исключение TypeError .
Можно заставить генератор выбросить исключение, вызвав его метод throw() и передав в качестве параметра значение исключения, которое должно быть выброшено. Это исключение будет выброшено из текущего приостановленного контекста генератора так, будто текущий приостановленный yield оператор являлся throw оператором.
Если yield оператор не встречается во время обработки выброшенного исключения, то исключение передаётся выше через вызов throw() , и результатом последующих вызовов next() будет свойство done равное true .
У генераторов есть метод return(value) , который возвращает заданное значение и останавливает работу генератора.
- « Предыдущая статья
- Следующая статья »
Found a content problem with this page?
- Edit the page on GitHub.
- Report the content issue.
- View the source on GitHub.
This page was last modified on 7 авг. 2023 г. by MDN contributors.
Зачем они нужны в JavaScript? Symbol, Iterator, Generator
Кратко, просто и понятно, что это такое и как это применять.
Начнём с Symbol
Что это?
Это новый примитивный тип данных, как boolean или string , который нужен для создания уникальных идентификаторов. Для простоты понимания представьте себе функцию которая возвращает всегда уникальный id, только в нашем случае вместо id – символ.
Пока всё просто. Теперь давайте посложнее.
Вот тут symbol становится немного странным, все другие примитивы могут стать объектами, если объявить их через new , а symbol не может. Почему так?
Это удерживает разработчиков от создания явного объекта-обёртки Symbol вместо нового символьного значения. Создание явных объектов-обёрток для примитивных типов доступно (например, new Boolean, new String, new Number).
Простыми словами — от символа мы хотим получить уникальный идентификатор, то есть сам Символ, а new возвращает объект, объект нам не нужен.
И последнее про Symbol
Существуют «глобальные символы», они доступны во всех частях вашей программы. То есть вы можете создать символ и поместить его в некую базу, это делается с помощью функции Symbol.for()
Если мы ещё раз вызовем Symbol.for(“Kanye West”) он вернёт существующий символ, а не новый.
Теперь к главному.
Зачем нужен Symbol ?
Представим, у вас есть некий объект в приложении, который используется много где, да и с ним работаете не только вы, он передаётся по разным функциям, над ним издеваются с помощью разных методов, и вдруг вам понадобилось запихнуть в этот объект свои данные, например логи.
Что вы будете делать — создадите новое поле объекта и присвоите ему свои логи. Но тут есть нюанс, если объект используется в большом количестве логики и другие разработчики как-то взаимодействуют с параметрами объекта, то есть шанс, что ваше новое поле где-то сможет сломать цикл, а где-то перезапишет собой другие поля.
Для решение этой проблемы, можно использовать symbol
Как видно, поле, ключом которого является symbol , нельзя обнаружить с помощью методов и конструкций вроде for..in . Поэтому новое свойство объекта не сломает приложение там, где используется логика похожая на ту, что выше.
Ну окей. Представим, вы вдруг решили добавить свойство __my_uniq_logs__ , вместо symbol , тогда смотрим что произойдёт дальше.
- При вызове метода Object.keys(myObj) вернётся дополнительное поле — ваши логи (а Object.keys обычно используется для прохода по массиву ключей)
- Новая не предвиденная переменная (ваши логи) в этом массиве ломает приложение
Так что, Symbol — ваш друг.
Также есть и более замороченные применения, про них можно почитать тут https://www.keithcirkel.co.uk/metaprogramming-in-es6-symbols/
Iterator
Что это?
В JavaScript есть так называемые итерируемые объекты, то есть объекты содержимое которых мы можем перебрать. Как например массив. Сам перебор, в большинстве своём, осуществляется с помощью итераторов (к примеру конструкция for..of для перебора элементов использует итераторы)
Давайте сделаем один.
А теперь используем наш итератор.
Что получается, итератор — это объект, который предоставляет метод next() , возвращающий следующий элемент последовательности.
Теперь более реальный пример.
У нас есть объект, который нужно «умно» перебрать.
Для того чтобы for..of выводил то, что мы хотим, нужно сделать объект range итерируемым.
Дальше без паники, всё объясню.
Symbol.iterator — что это ?
Как я говорил выше, конструкция for..of использует итераторы для перебора данных. В начале своего выполнения, for..of автоматически вызывает Symbol.iterator() для получения итератора и далее вызывает метод next() до получения < done: true >. Это внутренняя механика JavaScript, так уж он работает.
Зачем нужен Iterator ?
Когда хочешь перебирать объекты и другие типы данных по своему, итератор это отличный вариант.
Generator
Что это ?
Это функция которая может приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять своё выполнение в произвольный момент времени. Слегка трудновато, но позже всё станет понятно.
Так выглядят генераторы.
В общем, это обычная функция перед которой стоит *
Когда мы вызываем функцию-генератор, она возвращает нам объект-итератор. Вы с ним уже познакомились.
Вот как это работает на практике
Yield как бы говорит — передаём name и ставим паузу, пока не произойдёт следующий вызов next()
Также yield может принимать значения из вне.
Разберём код выше типичной ситуацией из жизни.
Представим что наш генератор это Канье Уэст который лично привёз вам домой свой альбом. Когда происходит первый вызов myIterator.next() Канье заходит к вам в дом и дарит свой альбом (передается name ). Канье человек простой, он хочет услышать в ответ благодарность и готов ждать её хоть вечность.
Вот, вы послушали альбом, подходите к Канье и говорите Спасибо (с помощью myIterator.next(‘West’) передаётся ‘West’ обратно в функцию), Канье принимает благодарность (переменной who присваивается ‘West’ ) и тут же уходит по своим делам. Такой уж Канье.
Зачем нужен Generator ?
В целом, его используют разные библиотеки как замену async/await для работы с асинхронными операциями. Сам async/await кстати, это high level абстракция над генераторами.
К примеру с помощью библиотеки co можно убрать callback hell и не использовать async/await .
Также популярная redux-saga использует генераторы.
В остальных случаях их применяют очень редко.
Суммируем
Symbols — новый уникальный тип данных. Обычно используется как свойство объекта, чтобы не поломать Object.keys и for-in .
Iterators — объект, который предоставляет метод next() , возвращающий следующий элемент последовательности. Обычно используется для кастомного перебора значений объектов в for-of и … (spread operator).
Generators — hight level абстракция над итераторами. Обычно используется как низкоуровневая альтернатива async/await .
И напоследок
Сегодня страшное стало понятным….слегка.
Если вы используете symbol, iterator и generator в своих проектах как-то по другому, пишите в комментариях, обязательно добавлю.
Если интересно как надо писать на React, то ниже есть гайд.
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 и такое есть.
В следующей статье: фича, которая прекрасно подойдёт к любому коду, который вы пишете каждый день. Присоединяйтесь на следующей неделе и мы рассмотрим шаблоны строк в деталях.