generators, fixes #63
335
1-js/10-es-modern/16-generator/article.md
Normal file
|
@ -0,0 +1,335 @@
|
|||
|
||||
# Генераторы
|
||||
|
||||
Генераторы -- новый вид функций в современном JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.
|
||||
|
||||
## Создание генератора
|
||||
|
||||
Для объявления генератора используется новая синтаксическая конструкция: `function*` (функция со звёздочкой).
|
||||
|
||||
Её называют "функция-генератор" (generator function).
|
||||
|
||||
Выглядит это так:
|
||||
|
||||
```js
|
||||
function* generateSequence() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
return 3;
|
||||
}
|
||||
```
|
||||
|
||||
При запуске `generateSequence()` код такой функции не выполняется!
|
||||
|
||||
Вместо этого она возвращает специальный объект, который как раз и называют "генератором".
|
||||
|
||||
```js
|
||||
// generator function создаёт generator
|
||||
let generator = generateSequence();
|
||||
```
|
||||
|
||||
Правильнее всего будет воспринимать генератор как "замороженный вызов функции":
|
||||
|
||||
<img src="generateSequence-1.png">
|
||||
|
||||
При создании генератора код находится в начале своего выполнения.
|
||||
|
||||
Основным методом генератора является `next()`. При вызове он возобновляет выполнение кода до ближайшего ключевого слова `yield`. По достижении `yield` выполнение приостанавливается, а значение -- возвращается во внешний код:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
let generator = generateSequence();
|
||||
|
||||
let one = generator.next();
|
||||
|
||||
alert(JSON.stringify(one)); // {value: 1, done: false}
|
||||
```
|
||||
|
||||
<img src="generateSequence-2.png">
|
||||
|
||||
Повторный вызов `generator.next()` возобновит выполнение и вернёт результат следующего `yield`:
|
||||
|
||||
```js
|
||||
let two = generator.next();
|
||||
|
||||
alert(JSON.stringify(two)); // {value: 2, done: false}
|
||||
```
|
||||
|
||||
<img src="generateSequence-3.png">
|
||||
|
||||
И, наконец, последний вызов завершит выполнение функции и вернёт результат `return`:
|
||||
|
||||
```js
|
||||
let three = generator.next();
|
||||
|
||||
alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*}
|
||||
```
|
||||
|
||||
<img src="generateSequence-4.png">
|
||||
|
||||
|
||||
Функция завершена. Внешний код должен увидить это из свойства `done:true` и прекратить вызовы. Впрочем, если новые вызовы `generator.next()` и будут, то они не вызовут ошибки, но будут возвращать один и тот же объект: `{done: true}`.
|
||||
|
||||
"Открутить назад" завершившийся генератор нельзя, но можно создать новый ещё одним вызовом `generateSequence()` и выполнить его.
|
||||
|
||||
## Генератор -- итератор
|
||||
|
||||
Как вы, наверно, уже догадались по наличию метода `next()`, генератор является итерируемым объектом.
|
||||
|
||||
Его можно перебирать и через `for..of`:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
return 3;
|
||||
}
|
||||
|
||||
let generator = generateSequence();
|
||||
|
||||
for(let value of generator) {
|
||||
alert(value); // 1, затем 2
|
||||
}
|
||||
```
|
||||
|
||||
Заметим, однако, существенную особенность такого перебора!
|
||||
|
||||
При запуске примера выше будет выведено значение `1`, затем `2`. Значение `3` выведено не будет. Это потому что стандартные перебор итератора игнорирует `value` на последнем значении, при `done: true`. Так что результат `return` в цикле `for..of` не выводится.
|
||||
|
||||
Соответственно, если мы хотим, чтобы все значения возвращались при переборе через `for..of`, то надо возвращать их через `yield`:
|
||||
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence() {
|
||||
yield 1;
|
||||
yield 2;
|
||||
*!*
|
||||
yield 3;
|
||||
*/!*
|
||||
}
|
||||
|
||||
let generator = generateSequence();
|
||||
|
||||
for(let value of generator) {
|
||||
alert(value); // 1, затем 2, затем 3
|
||||
}
|
||||
```
|
||||
|
||||
...А зачем вообще `return` при таком раскладе, если его результат игнорируется? Он тоже нужен, но в других ситуациях. Перебор через `for..of` -- в некотором смысле "исключение". Как мы увидим дальше, в других контекстах `return` очень даже востребован.
|
||||
|
||||
## Композиция генераторов
|
||||
|
||||
Один генератор может включать в себя другие. Это называется композицией.
|
||||
|
||||
Разберём композицию на примере.
|
||||
|
||||
Пусть у нас есть функция `generateSequence`, которая генерирует последовательность чисел:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence(start, end) {
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
yield i;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Используем оператор … для преобразования итерируемого объекта в массив
|
||||
let sequence = [...generateSequence(2,5)];
|
||||
|
||||
alert(sequence); // 2, 3, 4, 5
|
||||
```
|
||||
|
||||
Мы хотим на её основе сделать другую функцию `generateAlphaNumCodes()`, которая будет генерировать коды для буквенно-цифровых символов латинского алфавита:
|
||||
|
||||
<ul>
|
||||
<li>`48..57` -- для `0..9`</li>
|
||||
<li>`65..90` -- для `A..Z`</li>
|
||||
<li>`97..122` -- для `a..z`</li>
|
||||
</ul>
|
||||
|
||||
Далее этот набор кодов можно превратить в строку и использовать, к примеру, для выбора из него случайного пароля. Только символы пунктуации ещё хорошо бы добавить для надёжности, но в этом примере мы будем без них.
|
||||
|
||||
Естественно, раз в нашем распоряжении есть готовый генератор `generateSequence`, то хорошо бы его использовать.
|
||||
|
||||
Конечно, можно внутри `generateAlphaNum` запустить несколько `generateSequence`, объединить результаты и вернуть, но композиция -- это кое-что получше.
|
||||
|
||||
Она выглядит так:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence(start, end) {
|
||||
for (let i = start; i <= end; i++) yield i;
|
||||
}
|
||||
|
||||
function* generateAlphaNum() {
|
||||
|
||||
*!*
|
||||
// 0..9
|
||||
yield* generateSequence(48, 57);
|
||||
|
||||
// A..Z
|
||||
yield* generateSequence(65, 90);
|
||||
|
||||
// a..z
|
||||
yield* generateSequence(97, 122);
|
||||
*/!*
|
||||
|
||||
}
|
||||
|
||||
let str = '';
|
||||
|
||||
for(let code of generateAlphaNum()) {
|
||||
str += String.fromCharCode(code);
|
||||
}
|
||||
|
||||
alert(str); // 0..9A..Za..z
|
||||
```
|
||||
|
||||
Здесь использована специальная форма `yield*`. Она применима только к другому генератору и *делегирует* ему выполнение.
|
||||
|
||||
То есть, при `yield*` интерпретатор переходит внутрь генератора-аргумента, к примеру, `generateSequence(48, 57)`, выполняет его, и все `yield`, которые он делает, выходят из внешнего генератора.
|
||||
|
||||
Получается -- как будто мы вставили код внутреннего генератора во внешний напрямую, вот так:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* generateSequence(start, end) {
|
||||
for (let i = start; i <= end; i++) yield i;
|
||||
}
|
||||
|
||||
function* generateAlphaNum() {
|
||||
|
||||
*!*
|
||||
// yield* generateSequence(48, 57);
|
||||
for (let i = 48; i <= 57; i++) yield i;
|
||||
|
||||
// yield* generateSequence(65, 90);
|
||||
for (let i = 65; i <= 90; i++) yield i;
|
||||
|
||||
// yield* generateSequence(97, 122);
|
||||
for (let i = 97; i <= 122; i++) yield i;
|
||||
*/!*
|
||||
|
||||
}
|
||||
|
||||
let str = '';
|
||||
|
||||
for(let code of generateAlphaNum()) {
|
||||
str += String.fromCharCode(code);
|
||||
}
|
||||
|
||||
alert(str); // 0..9A..Za..z
|
||||
```
|
||||
|
||||
## yield -- дорога в обе стороны
|
||||
|
||||
До этого генераторы наиболее напоминали "итераторы на стероидах". Но, как мы сейчас увидим, это не так, есть фундаментальное различие, генераторы гораздо мощнее и гибче.
|
||||
|
||||
Всё дело в том, что `yield` -- дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор.
|
||||
|
||||
Вызов `let result = yield value` делает следующее:
|
||||
|
||||
<ul>
|
||||
<li>Возвращает `value` во внешний код, приостанавливая выполнение генератора.</li>
|
||||
<li>Внешний код может обработать значение, и затем вызвать `next` с аргументом: `generator.next(result)`.</li>
|
||||
<li>Генератор продолжит выполнение, этот аргумент будет записан в `result`.</li>
|
||||
</ul>
|
||||
|
||||
Продемонстрируем это на примере:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
function* gen() {
|
||||
*!*
|
||||
// Передать вопрос во внешний код и подождать ответа
|
||||
let result = yield "Сколько будет 2 + 2?";
|
||||
*/!*
|
||||
|
||||
alert(result);
|
||||
}
|
||||
|
||||
let generator = gen();
|
||||
|
||||
let question = generator.next().value;
|
||||
// { value: "Сколько будет 2 + 2?", done: false }
|
||||
|
||||
setTimeout(() => generator.next(4), 2000);
|
||||
```
|
||||
|
||||
<img src="genYield2.png">
|
||||
|
||||
Выше проиллюстрировано то, что происходит в генераторе:
|
||||
|
||||
<ol>
|
||||
<li>Первый `.next()` всегда без аргумента, он начинает выполнение и возвращает результат первого `yield`.</li>
|
||||
<li>Результат `yield` переходит во внешний код (в `question`), он может выполнять любые асинхронные задачи.</li>
|
||||
<li>Когда асинхронные задачи готовы, внешний код вызывает `.next(result)`, при этом выполнение продолжается, а `result` выходит из присваивания как результат `yield`.</li>
|
||||
</ol>
|
||||
|
||||
Посмотрим вариант побольше:
|
||||
|
||||
```js
|
||||
//+ run
|
||||
'use strict';
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
<img src="genYield2-2.png">
|
||||
|
||||
<ol>
|
||||
<li>Первый `.next()` начинает выполнение... Оно доходит до первого `yield`.</li>
|
||||
<li>Результат возвращается во внешний код.</li>
|
||||
<li>Второй `.next(4)` передаёт `4` обратно в генератор как результат первого `yield` и возобновляет выполнение.</li>
|
||||
<li>...Оно доходит до второго `yield`, который станет результатом `.next(4)`.</li>
|
||||
<li>Третий `next(9)` передаёт `9` в генератор как результат второго `yield` и возобновляет выполнение, которое завершается окончанием функции, так что `done: true`.</li>
|
||||
</ol>
|
||||
|
||||
Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`.
|
||||
|
||||
<img src="genYield2-3.png">
|
||||
|
||||
Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`.
|
||||
|
||||
|
||||
|
BIN
1-js/10-es-modern/16-generator/genYield2-2.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
1-js/10-es-modern/16-generator/genYield2-2@2x.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
1-js/10-es-modern/16-generator/genYield2-3.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
1-js/10-es-modern/16-generator/genYield2-3@2x.png
Normal file
After Width: | Height: | Size: 65 KiB |
BIN
1-js/10-es-modern/16-generator/genYield2.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
1-js/10-es-modern/16-generator/genYield2@2x.png
Normal file
After Width: | Height: | Size: 46 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-1.png
Normal file
After Width: | Height: | Size: 9 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-1@2x.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-2.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-2@2x.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-3.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-3@2x.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-4.png
Normal file
After Width: | Height: | Size: 14 KiB |
BIN
1-js/10-es-modern/16-generator/generateSequence-4@2x.png
Normal file
After Width: | Height: | Size: 29 KiB |
|
@ -215,7 +215,7 @@ document.getElementById('only-upper').onkeypress = function(e) {
|
|||
</script>
|
||||
[/online]
|
||||
|
||||
## Невосместимости [#keyboard-events-order]
|
||||
## Несовместимости [#keyboard-events-order]
|
||||
|
||||
Некоторые несовместимости в порядке срабатывания клавиатурных событий (когда что) ещё существуют.
|
||||
|
||||
|
|