Skip to content

Commit aa17c0d

Browse files
committed
generators, fixes iliakan#63
1 parent 5bb3c58 commit aa17c0d

20 files changed

Lines changed: 336 additions & 1 deletion
Lines changed: 335 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,335 @@
1+
2+
# Генераторы
3+
4+
Генераторы -- новый вид функций в современном JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.
5+
6+
## Создание генератора
7+
8+
Для объявления генератора используется новая синтаксическая конструкция: `function*` (функция со звёздочкой).
9+
10+
Её называют "функция-генератор" (generator function).
11+
12+
Выглядит это так:
13+
14+
```js
15+
function* generateSequence() {
16+
yield 1;
17+
yield 2;
18+
return 3;
19+
}
20+
```
21+
22+
При запуске `generateSequence()` код такой функции не выполняется!
23+
24+
Вместо этого она возвращает специальный объект, который как раз и называют "генератором".
25+
26+
```js
27+
// generator function создаёт generator
28+
let generator = generateSequence();
29+
```
30+
31+
Правильнее всего будет воспринимать генератор как "замороженный вызов функции":
32+
33+
<img src="generateSequence-1.png">
34+
35+
При создании генератора код находится в начале своего выполнения.
36+
37+
Основным методом генератора является `next()`. При вызове он возобновляет выполнение кода до ближайшего ключевого слова `yield`. По достижении `yield` выполнение приостанавливается, а значение -- возвращается во внешний код:
38+
39+
```js
40+
//+ run
41+
'use strict';
42+
43+
function* generateSequence() {
44+
yield 1;
45+
yield 2;
46+
return 3;
47+
}
48+
49+
let generator = generateSequence();
50+
51+
let one = generator.next();
52+
53+
alert(JSON.stringify(one)); // {value: 1, done: false}
54+
```
55+
56+
<img src="generateSequence-2.png">
57+
58+
Повторный вызов `generator.next()` возобновит выполнение и вернёт результат следующего `yield`:
59+
60+
```js
61+
let two = generator.next();
62+
63+
alert(JSON.stringify(two)); // {value: 2, done: false}
64+
```
65+
66+
<img src="generateSequence-3.png">
67+
68+
И, наконец, последний вызов завершит выполнение функции и вернёт результат `return`:
69+
70+
```js
71+
let three = generator.next();
72+
73+
alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*}
74+
```
75+
76+
<img src="generateSequence-4.png">
77+
78+
79+
Функция завершена. Внешний код должен увидить это из свойства `done:true` и прекратить вызовы. Впрочем, если новые вызовы `generator.next()` и будут, то они не вызовут ошибки, но будут возвращать один и тот же объект: `{done: true}`.
80+
81+
"Открутить назад" завершившийся генератор нельзя, но можно создать новый ещё одним вызовом `generateSequence()` и выполнить его.
82+
83+
## Генератор -- итератор
84+
85+
Как вы, наверно, уже догадались по наличию метода `next()`, генератор является итерируемым объектом.
86+
87+
Его можно перебирать и через `for..of`:
88+
89+
```js
90+
//+ run
91+
'use strict';
92+
93+
function* generateSequence() {
94+
yield 1;
95+
yield 2;
96+
return 3;
97+
}
98+
99+
let generator = generateSequence();
100+
101+
for(let value of generator) {
102+
alert(value); // 1, затем 2
103+
}
104+
```
105+
106+
Заметим, однако, существенную особенность такого перебора!
107+
108+
При запуске примера выше будет выведено значение `1`, затем `2`. Значение `3` выведено не будет. Это потому что стандартные перебор итератора игнорирует `value` на последнем значении, при `done: true`. Так что результат `return` в цикле `for..of` не выводится.
109+
110+
Соответственно, если мы хотим, чтобы все значения возвращались при переборе через `for..of`, то надо возвращать их через `yield`:
111+
112+
113+
```js
114+
//+ run
115+
'use strict';
116+
117+
function* generateSequence() {
118+
yield 1;
119+
yield 2;
120+
*!*
121+
yield 3;
122+
*/!*
123+
}
124+
125+
let generator = generateSequence();
126+
127+
for(let value of generator) {
128+
alert(value); // 1, затем 2, затем 3
129+
}
130+
```
131+
132+
...А зачем вообще `return` при таком раскладе, если его результат игнорируется? Он тоже нужен, но в других ситуациях. Перебор через `for..of` -- в некотором смысле "исключение". Как мы увидим дальше, в других контекстах `return` очень даже востребован.
133+
134+
## Композиция генераторов
135+
136+
Один генератор может включать в себя другие. Это называется композицией.
137+
138+
Разберём композицию на примере.
139+
140+
Пусть у нас есть функция `generateSequence`, которая генерирует последовательность чисел:
141+
142+
```js
143+
//+ run
144+
'use strict';
145+
146+
function* generateSequence(start, end) {
147+
148+
for (let i = start; i <= end; i++) {
149+
yield i;
150+
}
151+
152+
}
153+
154+
// Используем оператор … для преобразования итерируемого объекта в массив
155+
let sequence = [...generateSequence(2,5)];
156+
157+
alert(sequence); // 2, 3, 4, 5
158+
```
159+
160+
Мы хотим на её основе сделать другую функцию `generateAlphaNumCodes()`, которая будет генерировать коды для буквенно-цифровых символов латинского алфавита:
161+
162+
<ul>
163+
<li>`48..57` -- для `0..9`</li>
164+
<li>`65..90` -- для `A..Z`</li>
165+
<li>`97..122` -- для `a..z`</li>
166+
</ul>
167+
168+
Далее этот набор кодов можно превратить в строку и использовать, к примеру, для выбора из него случайного пароля. Только символы пунктуации ещё хорошо бы добавить для надёжности, но в этом примере мы будем без них.
169+
170+
Естественно, раз в нашем распоряжении есть готовый генератор `generateSequence`, то хорошо бы его использовать.
171+
172+
Конечно, можно внутри `generateAlphaNum` запустить несколько `generateSequence`, объединить результаты и вернуть, но композиция -- это кое-что получше.
173+
174+
Она выглядит так:
175+
176+
```js
177+
//+ run
178+
'use strict';
179+
180+
function* generateSequence(start, end) {
181+
for (let i = start; i <= end; i++) yield i;
182+
}
183+
184+
function* generateAlphaNum() {
185+
186+
*!*
187+
// 0..9
188+
yield* generateSequence(48, 57);
189+
190+
// A..Z
191+
yield* generateSequence(65, 90);
192+
193+
// a..z
194+
yield* generateSequence(97, 122);
195+
*/!*
196+
197+
}
198+
199+
let str = '';
200+
201+
for(let code of generateAlphaNum()) {
202+
str += String.fromCharCode(code);
203+
}
204+
205+
alert(str); // 0..9A..Za..z
206+
```
207+
208+
Здесь использована специальная форма `yield*`. Она применима только к другому генератору и *делегирует* ему выполнение.
209+
210+
То есть, при `yield*` интерпретатор переходит внутрь генератора-аргумента, к примеру, `generateSequence(48, 57)`, выполняет его, и все `yield`, которые он делает, выходят из внешнего генератора.
211+
212+
Получается -- как будто мы вставили код внутреннего генератора во внешний напрямую, вот так:
213+
214+
```js
215+
//+ run
216+
'use strict';
217+
218+
function* generateSequence(start, end) {
219+
for (let i = start; i <= end; i++) yield i;
220+
}
221+
222+
function* generateAlphaNum() {
223+
224+
*!*
225+
// yield* generateSequence(48, 57);
226+
for (let i = 48; i <= 57; i++) yield i;
227+
228+
// yield* generateSequence(65, 90);
229+
for (let i = 65; i <= 90; i++) yield i;
230+
231+
// yield* generateSequence(97, 122);
232+
for (let i = 97; i <= 122; i++) yield i;
233+
*/!*
234+
235+
}
236+
237+
let str = '';
238+
239+
for(let code of generateAlphaNum()) {
240+
str += String.fromCharCode(code);
241+
}
242+
243+
alert(str); // 0..9A..Za..z
244+
```
245+
246+
## yield -- дорога в обе стороны
247+
248+
До этого генераторы наиболее напоминали "итераторы на стероидах". Но, как мы сейчас увидим, это не так, есть фундаментальное различие, генераторы гораздо мощнее и гибче.
249+
250+
Всё дело в том, что `yield` -- дорога в обе стороны: он не только возвращает результат наружу, но и может передавать значение извне в генератор.
251+
252+
Вызов `let result = yield value` делает следующее:
253+
254+
<ul>
255+
<li>Возвращает `value` во внешний код, приостанавливая выполнение генератора.</li>
256+
<li>Внешний код может обработать значение, и затем вызвать `next` с аргументом: `generator.next(result)`.</li>
257+
<li>Генератор продолжит выполнение, этот аргумент будет записан в `result`.</li>
258+
</ul>
259+
260+
Продемонстрируем это на примере:
261+
262+
```js
263+
//+ run
264+
'use strict';
265+
266+
function* gen() {
267+
*!*
268+
// Передать вопрос во внешний код и подождать ответа
269+
let result = yield "Сколько будет 2 + 2?";
270+
*/!*
271+
272+
alert(result);
273+
}
274+
275+
let generator = gen();
276+
277+
let question = generator.next().value;
278+
// { value: "Сколько будет 2 + 2?", done: false }
279+
280+
setTimeout(() => generator.next(4), 2000);
281+
```
282+
283+
<img src="genYield2.png">
284+
285+
Выше проиллюстрировано то, что происходит в генераторе:
286+
287+
<ol>
288+
<li>Первый `.next()` всегда без аргумента, он начинает выполнение и возвращает результат первого `yield`.</li>
289+
<li>Результат `yield` переходит во внешний код (в `question`), он может выполнять любые асинхронные задачи.</li>
290+
<li>Когда асинхронные задачи готовы, внешний код вызывает `.next(result)`, при этом выполнение продолжается, а `result` выходит из присваивания как результат `yield`.</li>
291+
</ol>
292+
293+
Посмотрим вариант побольше:
294+
295+
```js
296+
//+ run
297+
'use strict';
298+
299+
function* gen() {
300+
let ask1 = yield "Сколько будет 2 + 2?";
301+
302+
alert(ask1); // 4
303+
304+
let ask2 = yield "А сколько будет 3 * 3?"
305+
306+
alert(ask2); // 9
307+
}
308+
309+
let generator = gen();
310+
311+
alert( generator.next().value ); // "...2+2?"
312+
313+
alert( generator.next(4).value ); // "...3*3?"
314+
315+
alert( generator.next(9).done ); // true
316+
```
317+
318+
<img src="genYield2-2.png">
319+
320+
<ol>
321+
<li>Первый `.next()` начинает выполнение... Оно доходит до первого `yield`.</li>
322+
<li>Результат возвращается во внешний код.</li>
323+
<li>Второй `.next(4)` передаёт `4` обратно в генератор как результат первого `yield` и возобновляет выполнение.</li>
324+
<li>...Оно доходит до второго `yield`, который станет результатом `.next(4)`.</li>
325+
<li>Третий `next(9)` передаёт `9` в генератор как результат второго `yield` и возобновляет выполнение, которое завершается окончанием функции, так что `done: true`.</li>
326+
</ol>
327+
328+
Получается "пинг-понг": каждый `next(value)` передаёт в генератор значение, которое становится результатом текущего `yield`, возобновляет выполнение и получает выражение из следующего `yield`.
329+
330+
<img src="genYield2-3.png">
331+
332+
Исключением является первый вызов `next`, который не может передать значение в генератор, т.к. ещё не было ни одного `yield`.
333+
334+
335+
35.5 KB
Loading
81.4 KB
Loading
29.2 KB
Loading
65.4 KB
Loading
20.6 KB
Loading
46.3 KB
Loading
8.98 KB
Loading
21.4 KB
Loading
13.4 KB
Loading

0 commit comments

Comments
 (0)