Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 65 additions & 73 deletions 1-js/12-generators-iterators/1-generators/article.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Generators

通常情况下,函数都只会返回一个值或者什么也不返回
常规函数只会返回一个具体值(或者 `undefined`)

Generators 可以按需一个个返回(“yield”)多个值,可以是无限数量个值。它们与 [iterables](info:iterable) 配合使用,可以轻松创建数据流。
Generators 可以按需逐个生成(“yield”)多个值。它们与 [iterables](info:iterable) 配合使用,可以轻松创建数据流。

## Generator 函数

Expand All @@ -18,24 +18,35 @@ function* generateSequence() {
}
```

“generator 函数”这个术语听起来有点误导,因为我们在调用它时候并不会执行代码。相反,他返回一个特殊的对象,我们称为“generator 对象”。
“generator 函数”与常规函数的运行表现有所不同,当执行“generator 函数”时,它并不直接执行完**函数体**的代码,而是返回一个特殊的对象,即“generator 对象”,来管理执行流程

因此,它是一种“generator 构造器函数”。
来打印一下这种对象:

```js
// “generator 函数”创建“generator 对象”
function* generateSequence() {
yield 1;
yield 2;
return 3;
}

// "generator function"(指 generateSequence()) 创建了一个 "generator 对象"
let generator = generateSequence();
*!*
alert(generator); // [object Generator]
*/!*
```

`generator` 对象类似于“冻结函数调用(frozen function call)”:
上面的代码中,**函数体**代码还没有开始执行:

![](generateSequence-1.svg)

在创建后,代码在一开始就暂停执行
generator 对象的主要方法是 `next()`。被调用时,它会恢复上面的执行过程直到最近的 `yield <value>` 语句( `value` 可以省略,默认为 `undefined` )。然后代码再次暂停执行,并将值返回给外部代码

Generator 的主要方法是 `next()`。调用它后,就会恢复上面的执行过程直到最近的 `yield <value>` 语句。然后代码再次暂停执行,并将值返回到外部代码。
`next()` 调用结果总是一个包含两个属性的对象:
- `value`: “generator 函数”每次 **产出(yielded)** 的值。(译者注:yield翻译为产出,是为了配合 **生成器(generator)** 的语义。)
- `done`: `true` 表示“generator 函数”已经执行完成,否则为 `false`。

例如,这里我们创建了 generator 并获取其第一个 yielded 值
举个例子,下面我们创建一个 generator 并获取其第一个产出的值

```js run
function* generateSequence() {
Expand All @@ -53,15 +64,11 @@ let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}
```

`next()` 的结果总是一个对象:
- `value`:yielded 值。
- `done`:如果代码没有执行完,其值为 `false`,否则就是 `true`。

截至目前,我们只获得了第一个值:
截至目前,我们只获得了第一个值,函数体停在了第二行:

![](generateSequence-2.svg)

我们再次调用 `generator.next()`。代码恢复执行并返回下一个 `yield`:
再次调用 `generator.next()`。代码恢复执行并返回下一个 `yield` 的产出值

```js
let two = generator.next();
Expand All @@ -81,23 +88,19 @@ alert(JSON.stringify(three)); // {value: 3, *!*done: true*/!*}

![](generateSequence-4.svg)

现在,generator 已经执行完成了。我们通过 `done:true` 和处理的最终结果 `value:3` 可以看出来。

此时如果再调用 `generator.next()` 将不起任何作用。如果我们还是执行此语句,那么它将会返回相同的对象:`{done: true}`。
我们通过 `done:true` 可以看出函数执行完成了,此时 `value:3` 作为函数执行的最终结果。

对于 generator 来说,没有办法去“回滚”它的操作。但是我们可以通过调用 `generateSequence()` 来创建另一个 generator。

到目前为止,最重要的是要理解 generator 函数与常规函数不同,generator 函数不运行代码。它们是作为“generator 工厂”。运行 `function*` 返回一个 generator,然后我们调用它获得需要的值。
再调用 `generator.next()` 已经没有什么意义了。这将总是返回相同的对象:`{done: true}`。

```smart header="`function* f(…)` 或者 `function *f(…)`?"
这是一个小的书写习惯问题,两者的语法都是正确的。

但是通常首选第一种语法,因为星号 `*` 表示它是一个 generator 函数,它描述的是函数种类而不是名称,因此它仍应使用 `function` 关键字
但是通常首选第一种语法,因为星号 `*` 表示它是一个 generator 函数,它描述的是函数种类而不是名称,因此`*`应该和 `function` 关键字紧贴一起
```

## Generators 是可迭代的

你可能通过 `next()` 方法了解到 generator 是[可迭代](info:iterable)的。
看到 `next()` 方法,或许你都猜到了 generator 是 [可迭代](info:iterable) 的。(译者注:`next()` 是 iterator 的必要方法)

我们可以通过 `for..of` 循环迭代所有值:

Expand All @@ -115,11 +118,11 @@ for(let value of generator) {
}
```

这样的方法看上去要比一个个调用 `.next().value` 好得多,不是吗
`for ... of` 写法是不是比 `.next().value` 优雅多了

……但是请注意:上面的迭代例子中,它先显示 `1`,然后是 `2`。它不会显示 `3`!

这是因为当 `done: true` 时,for-of 循环会忽略最后一个 `value`。因此,如果我们想要通过 `for..of` 循环显示所有结果,我们必须使用 `yield` 而不是 `return` 返回它们
这是因为当 `done: true` 时,for-of 循环会忽略最后一个 `value`。因此,如果我们想要通过 `for..of` 循环显示所有结果时,我们必须使用 `yield` 而不是 `return`:

```js run
function* generateSequence() {
Expand All @@ -137,7 +140,7 @@ for(let value of generator) {
}
```

当然,由于 generators 是可迭代的,我们可以调用所有相关的函数,例如:spread 操作 `...`:
由于 generators 是可迭代的,我们可以充分发挥 ES6 中 iterator 的特性,例如:spread 操作 `...`:

```js run
function* generateSequence() {
Expand All @@ -151,7 +154,7 @@ let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
```

在上面的代码中,`...generateSequence()` 将 iterable 转换为 item 的数组(关于 spread 操作可以参见相关章节 [](info:rest-parameters-spread-operator#spread-operator))。
在上面的代码中,`...generateSequence()` 将可迭代的“generator 对象”转换为了一个数组(关于 spread 操作可以参见相关章节 [](info:rest-parameters-spread-operator#spread-operator))。

## 使用 generator 进行迭代

Expand All @@ -167,14 +170,14 @@ let range = {
// for..of range 在一开始就调用一次这个方法
[Symbol.iterator]() {
// ……它返回 iterator 对象:
// 向前,for..of 仅适用于该对象,请求下一个值
// 后续的操作中, for..of 将只针对这个 iterator 对象,通过不断的调用 next() 来获取下一个值
return {
current: this.from,
last: this.to,

// for..of 在每次迭代的时候都会调用 next()
next() {
// 它应该返回对象 {done:.., value :...}
// 必须返回特定结构的对象: {done:.., value :...}
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
Expand All @@ -185,28 +188,13 @@ let range = {
}
};

// 迭代整个 range 对象,返回从 `range.from` 到 `range.to` 范围的所有数字的数组。
alert([...range]); // 1,2,3,4,5
```

使用 generator 来生成可迭代序列更简单,更优雅:

```js run
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}

let sequence = [...generateSequence(1,5)];

alert(sequence); // 1, 2, 3, 4, 5
```

## 转换 Symbol.iterator 为 generator

我们可以通过提供一个 generator 作为 `Symbol.iterator` 来向任何自定义对象添加 generator-style 的迭代。
我们可以通过提供一个 generator 函数作为对象的 `Symbol.iterator` 来使任何对象可被迭代。

这是相同的 `range`,但是使用的是一个更紧凑的 iterator
以下是使用的另一种更紧凑的写法的 `range` 对象

```js run
let range = {
Expand All @@ -223,15 +211,15 @@ let range = {
alert( [...range] ); // 1,2,3,4,5
```

正常工作,因为 `range[Symbol.iterator]()` 现在返回一个 generator,而 generator 方法正是 `for..of` 所期待的
代码正常工作,因为 `range[Symbol.iterator]()` 现在返回一个 generator,而 generator 方法正是 `for..of` 所期望的
- 它具有 `.next()` 方法
- 它以 `{value: ..., done: true/false}` 的形式返回值

当然,这不是巧合,Generators 被添加到 JavaScript 语言中时也考虑了 iterators,以便更容易实现。

带有 generator 的最后一个变体比 `range` 的原始可迭代代码简洁得多,并保持了相同的功能
带有 generator `range` 对象比的原始可迭代代码简洁得多,还保持了功能的一致

```smart header="Generators 可能永远 generate 值"
```smart header="Generators 可以永远产生值"
在上面的例子中,我们生成了有限序列,但是我们也可以创建一个生成无限序列的 generator,它可以一直 yield 值。例如,无序的伪随机数序列。

这种情况下的 `for..of` generator 需要一个 `break`(或者 `return`)语句,否则循环将永远重复并挂起。
Expand All @@ -241,18 +229,26 @@ alert( [...range] ); // 1,2,3,4,5

Generator 组合是 generator 的一个特殊功能,它可以显式地将 generator “嵌入”到一起。

例如,我们想要生成一个序列:
- 数字 `0..9`(ASCII 可显示字符代码为 48..57),
- 后跟字母 `a..z`(ASCII 可显示字符代码为 65..90)
- 后跟大写字母 `A..Z`(ASCII 可显示字符代码为 97..122)
如下,我们有个生成数字序列的函数:

我们可以使用序列,比如通过从中选择字符来创建密码(也可以添加语法字符),但是我们先生成它。
```js
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
```

接着,我们复用这个函数来生成更加复杂的包含三部分的序列:
- 第一部分为数字 `0..9`(ASCII 可显示字符代码为 48..57),
- 第二部分为大写字母字母 `A..Z`(ASCII 可显示字符代码为 65..90)
- 第三部分为小写字母 `a...z`(ASCII 可显示字符代码为 97..122)

我们已经有了 `function* generateSequence(start, end)`。让我们重复使用它来一个个地传递 3 个序列,它真是我们所需要的
我们可以在这个序列中选择字符来创建密码(也可以添加其他特殊字符),现在先编写这个生成器

在普通函数中,为了将多个其他函数的结果组合到一起,我们先调用它们,然后将他们的结果存储起来,最后将它们合并到一起
在常规函数的调用中,为了组合多个函数的结果,我们需要先依次调用它们,并分别将他们的结果存储起来,最后统一将它们合并到一起

对于 generators,我们可以更好地去实现,就像这样:
对于 generators 而言,我们可以使用 `yield*` 这个语法来将一个 generator 嵌入(组合)到另一个 generator 中:

组合式 generator 的例子:

```js run
function* generateSequence(start, end) {
Expand Down Expand Up @@ -283,9 +279,9 @@ for(let code of generatePasswordCodes()) {
alert(str); // 0..9A..Za..z
```

示例中的特殊 `yield*` 指令负责组合。它将执行**委托**给另一个 generator。或者简单来说就是 `yield* gen` 迭代 generator `gen` 并显式地将其 yield 结果转发到外部。好像这些值是由外部 generator yield 一样
示例中的 `yield*` 指令负责将执行**委托**给另一个 generator。或者简单来说就是 `yield* gen` 迭代了名为 `gen` 的 generator 并显式地将 `gen` yield 的结果转发到最外部。好像这些值是由外部的 generator yield 的一样

结果就像是我们从嵌套的 generators 内联的代码一样
执行结果和我们将嵌套的 generators 中的代码直接内联到外层generator一样

```js run
function* generateSequence(start, end) {
Expand Down Expand Up @@ -316,19 +312,15 @@ for(let code of generateAlphaNum()) {
alert(str); // 0..9A..Za..z
```

Generator 组合是将一个 generator 流插入到另一个 generator 的自然的方式。

即使来自嵌套 generator 的值的流是无限的,它也可以正常工作。它很简单,不需要使用额外的内存来存储中间结果。

## “yield” 双向路径(two-way road)
**Generator 组合**是将一个 generator 流自然地插入到另一个 generator 的方式。它不需要使用额外的内存来存储中间结果。

直到此时,generators 就像“固醇(steroids)上的 iterators”。这就是它经常被使用的方式。
## “yield” 双向路径(two-way street)

但是实际上,generators 要更强大,更灵活
目前看来,generators 和可迭代对象非常相似,仅仅是其产生 value 的语法有所不同。但实际上,generators 更加高效和灵活

这是因为 `yield` 是一个双向路径:它不仅向外面返回结果,而且可以传递 generator 内的值
这是因为 `yield` 是一个双向路径:它不仅向外面返回结果,而且可以将外部的值传递到 generator

为此,我们应该使用参数 arg 调用 `generator.next(arg)`。这个参数就成了 `yield` 的结果
调用 `generator.next(arg)`,我们就能将值 `arg` 传递到了 generator 内部。这个 `arg` 参数会变成 `yield` 语句的结果(返回值)

我们来看一个例子:

Expand Down Expand Up @@ -364,9 +356,9 @@ generator.next(4); // --> 向 generator 传入结果
setTimeout(() => generator.next(4), 1000);
```

我们可以看到,与普通函数不同,generators 和调用代码可以通过传递 `next/yield` 中的值来交换结果
我们可以看到,与常规函数不同,generators 内部和外部调用环境可以通过 `next/yield` 来传递值,以交换结果

为了使事情浅显易懂,我们来看另一个有更多调用的例子:
为了使以上要点浅显易懂,我们来看另一个有更多调用的例子:

```js run
function* gen() {
Expand Down Expand Up @@ -406,7 +398,7 @@ alert( generator.next(9).done ); // true

……但是它也可以在那里发起(抛出)错误。这很自然,因为错误本身也是一种结果。

要向 `yield` 传递错误,我们应该调用 `generator.throw(err)`。在那种情况下,`err` `yield` 一起被抛出
要向 `yield` 传递错误,我们应该调用 `generator.throw(err)`。然后,`err` 将在对应的 `yield` 那一行被抛出

例如,`"2 + 2?"` 的 yield 导致一个错误:

Expand Down Expand Up @@ -454,7 +446,7 @@ try {
*/!*
```

如果我们在那里捕获错误,那么像往常一样,它会转到外部代码(如果有的话),如果没有捕获,则会结束脚本。
通常,如果我们没有在那里捕获错误,它会将错误转到外部代码(如果有的话),如果外部也没有捕获错误,则会结束脚本。

## 总结

Expand Down