1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00

docs(generator): edit typo

This commit is contained in:
ruanyf 2017-04-29 06:33:43 +08:00
parent e2867f1956
commit be1049f12a
2 changed files with 122 additions and 126 deletions

View File

@ -1228,7 +1228,7 @@ function f(x){
} }
``` ```
上面代码中函数f的最后一步是调用函数g这就叫尾调用。 上面代码中,函数`f`的最后一步是调用函数`g`,这就叫尾调用。
以下三种情况,都不属于尾调用。 以下三种情况,都不属于尾调用。
@ -1250,7 +1250,7 @@ function f(x){
} }
``` ```
上面代码中情况一是调用函数g之后还有赋值操作所以不属于尾调用即使语义完全一样。情况二也属于调用后还有操作即使写在一行内。情况三等同于下面的代码。 上面代码中,情况一是调用函数`g`之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。
```javascript ```javascript
function f(x){ function f(x){
@ -1270,13 +1270,13 @@ function f(x) {
} }
``` ```
上面代码中函数m和n都属于尾调用因为它们都是函数f的最后一步操作。 上面代码中,函数`m``n`都属于尾调用,因为它们都是函数`f`的最后一步操作。
### 尾调用优化 ### 尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。 尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道函数调用会在内存形成一个“调用记录”又称“调用帧”call frame保存调用位置和内部变量等信息。如果在函数A的内部调用函数B那么在A的调用帧上方还会形成一个B的调用帧。等到B运行结束将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C那就还有一个C的调用帧以此类推。所有的调用帧就形成一个“调用栈”call stack 我们知道函数调用会在内存形成一个“调用记录”又称“调用帧”call frame保存调用位置和内部变量等信息。如果在函数`A`的内部调用函数`B`,那么在`A`的调用帧上方,还会形成一个`B`的调用帧。等到`B`运行结束,将结果返回到`A``B`的调用帧才会消失。如果函数`B`内部还调用函数`C`,那就还有一个`C`的调用帧以此类推。所有的调用帧就形成一个“调用栈”call stack
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
@ -1298,7 +1298,7 @@ f();
g(3); g(3);
``` ```
上面代码中如果函数g不是尾调用函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后函数f就结束了所以执行到最后一步完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。 上面代码中,如果函数`g`不是尾调用,函数`f`就需要保存内部变量`m``n`的值、`g`的调用位置等信息。但由于调用`g`之后,函数`f`就结束了,所以执行到最后一步,完全可以删除`f(x)`的调用帧,只保留`g(3)`的调用帧。
这就叫做“尾调用优化”Tail call optimization即只保留内层函数的调用帧。如果所有函数都是尾调用那么完全可以做到每次执行时调用帧只有一项这将大大节省内存。这就是“尾调用优化”的意义。 这就叫做“尾调用优化”Tail call optimization即只保留内层函数的调用帧。如果所有函数都是尾调用那么完全可以做到每次执行时调用帧只有一项这将大大节省内存。这就是“尾调用优化”的意义。
@ -1331,7 +1331,7 @@ function factorial(n) {
factorial(5) // 120 factorial(5) // 120
``` ```
上面代码是一个阶乘函数计算n的阶乘最多需要保存n个调用记录复杂度 O(n) 。 上面代码是一个阶乘函数,计算`n`的阶乘,最多需要保存`n`个调用记录,复杂度 O(n) 。
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
@ -1344,9 +1344,9 @@ function factorial(n, total) {
factorial(5, 1) // 120 factorial(5, 1) // 120
``` ```
还有一个比较著名的例子,就是计算fibonacci 数列,也能充分说明尾递归优化的重要性 还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性
如果是非尾递归的fibonacci 递归方法 非尾递归的 Fibonacci 数列实现如下。
```javascript ```javascript
function Fibonacci (n) { function Fibonacci (n) {
@ -1355,13 +1355,12 @@ function Fibonacci (n) {
return Fibonacci(n - 1) + Fibonacci(n - 2); return Fibonacci(n - 1) + Fibonacci(n - 2);
} }
Fibonacci(10); // 89 Fibonacci(10) // 89
// Fibonacci(100) Fibonacci(100) // 堆栈溢出
// Fibonacci(500) Fibonacci(500) // 堆栈溢出
// 堆栈溢出了
``` ```
如果我们使用尾递归优化过的fibonacci 递归算法 尾递归优化过的 Fibonacci 数列实现如下。
```javascript ```javascript
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
@ -1375,11 +1374,11 @@ Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity Fibonacci2(10000) // Infinity
``` ```
由此可见“尾调用优化”对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。ES6也是如此第一次明确规定所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,ES6中只要使用尾递归,就不会发生栈溢出,相对节省内存。 由此可见“尾调用优化”对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现都必须部署“尾调用优化”。这就是说ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
### 递归函数的改写 ### 递归函数的改写
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total 那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观第一眼很难看出来为什么计算5的阶乘需要传入两个参数5和1 尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量`total`,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算`5`的阶乘,需要传入两个参数`5``1`
两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。 两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。
@ -1396,7 +1395,7 @@ function factorial(n) {
factorial(5) // 120 factorial(5) // 120
``` ```
上面代码通过一个正常形式的阶乘函数 factorial ,调用尾递归函数 tailFactorial ,看起来就正常多了。 上面代码通过一个正常形式的阶乘函数`factorial`,调用尾递归函数`tailFactorial`,看起来就正常多了。
函数式编程有一个概念叫做柯里化currying意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 函数式编程有一个概念叫做柯里化currying意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。
@ -1417,9 +1416,9 @@ const factorial = currying(tailFactorial, 1);
factorial(5) // 120 factorial(5) // 120
``` ```
上面代码通过柯里化,将尾递归函数 tailFactorial 变为只接受1个参数的 factorial 上面代码通过柯里化,将尾递归函数`tailFactorial`变为只接受一个参数的`factorial`
第二种方法就简单多了就是采用ES6的函数默认值。 第二种方法就简单多了,就是采用 ES6 的函数默认值。
```javascript ```javascript
function factorial(n, total = 1) { function factorial(n, total = 1) {
@ -1430,13 +1429,13 @@ function factorial(n, total = 1) {
factorial(5) // 120 factorial(5) // 120
``` ```
上面代码中,参数 total 有默认值1,所以调用时不用提供这个值。 上面代码中,参数`total`有默认值`1`,所以调用时不用提供这个值。
总结一下递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令所有的循环都用递归实现这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言比如LuaES6只需要知道循环可以用递归代替而一旦使用递归就最好使用尾递归。 总结一下递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令所有的循环都用递归实现这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言比如LuaES6只需要知道循环可以用递归代替而一旦使用递归就最好使用尾递归。
### 严格模式 ### 严格模式
ES6的尾调用优化只在严格模式下开启正常模式是无效的。 ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。 这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
@ -1447,7 +1446,7 @@ ES6的尾调用优化只在严格模式下开启正常模式是无效的。
```javascript ```javascript
function restricted() { function restricted() {
"use strict"; 'use strict';
restricted.caller; // 报错 restricted.caller; // 报错
restricted.arguments; // 报错 restricted.arguments; // 报错
} }

View File

@ -4,13 +4,13 @@
### 基本概念 ### 基本概念
Generator 函数是 ES6 提供的一种异步编程解决方案语法行为与传统函数完全不同。本章详细介绍Generator 函数的语法和 API它的异步编程应用请看《Generator 函数的异步应用》一章。 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API它的异步编程应用请看《Generator 函数的异步应用》一章。
Generator 函数有多种理解角度。从语法上首先可以把它理解成Generator 函数是一个状态机,封装了多个内部状态。 Generator 函数有多种理解角度。从语法上首先可以把它理解成Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象也就是说Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 执行 Generator 函数会返回一个遍历器对象也就是说Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
形式上Generator 函数是一个普通函数,但是有两个特征。一是,`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`语句,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。 形式上Generator 函数是一个普通函数,但是有两个特征。一是,`function`关键字与函数名之间有一个星号;二是,函数体内部使用`yield`表达式,定义不同的内部状态(`yield`在英语里的意思就是“产出”)。
```javascript ```javascript
function* helloWorldGenerator() { function* helloWorldGenerator() {
@ -22,11 +22,11 @@ function* helloWorldGenerator() {
var hw = helloWorldGenerator(); var hw = helloWorldGenerator();
``` ```
上面代码定义了一个Generator函数`helloWorldGenerator`,它内部有两个`yield`语句“hello”和“world”即该函数有三个状态helloworld和return语句(结束执行)。 上面代码定义了一个 Generator 函数`helloWorldGenerator`,它内部有两个`yield`表达式(`hello``world`即该函数有三个状态helloworld 和 return 语句(结束执行)。
然后Generator函数的调用方法与普通函数一样也是在函数名后面加上一对圆括号。不同的是调用Generator函数后该函数并不执行返回的也不是函数运行结果而是一个指向内部状态的指针对象也就是上一章介绍的遍历器对象Iterator Object 然后Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后该函数并不执行返回的也不是函数运行结果而是一个指向内部状态的指针对象也就是上一章介绍的遍历器对象Iterator Object
下一步必须调用遍历器对象的next方法使得指针移向下一个状态。也就是说每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`语句(或`return`语句为止。换言之Generator函数是分段执行的`yield`语句是暂停执行的标记,而`next`方法可以恢复执行。 下一步,必须调用遍历器对象的`next`方法,使得指针移向下一个状态。也就是说,每次调用`next`方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个`yield`表达式(或`return`语句为止。换言之Generator 函数是分段执行的,`yield`表达式是暂停执行的标记,而`next`方法可以恢复执行。
```javascript ```javascript
hw.next() hw.next()
@ -44,45 +44,42 @@ hw.next()
上面代码一共调用了四次`next`方法。 上面代码一共调用了四次`next`方法。
第一次调用Generator函数开始执行直到遇到第一个`yield`语句为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`语句的值hello`done`属性的值false,表示遍历还没有结束。 第一次调用Generator 函数开始执行,直到遇到第一个`yield`表达式为止。`next`方法返回一个对象,它的`value`属性就是当前`yield`表达式的值`hello``done`属性的值`false`,表示遍历还没有结束。
第二次调用Generator函数从上次`yield`语句停下的地方,一直执行到下一个`yield`语句。`next`方法返回的对象的`value`属性就是当前`yield`语句的值world`done`属性的值false,表示遍历还没有结束。 第二次调用Generator 函数从上次`yield`表达式停下的地方,一直执行到下一个`yield`表达式。`next`方法返回的对象的`value`属性就是当前`yield`表达式的值`world``done`属性的值`false`,表示遍历还没有结束。
第三次调用Generator函数从上次`yield`语句停下的地方,一直执行到`return`语句如果没有return语句就执行到函数结束`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为undefined`done`属性的值true表示遍历已经结束。 第三次调用Generator 函数从上次`yield`表达式停下的地方,一直执行到`return`语句(如果没有`return`语句,就执行到函数结束)。`next`方法返回的对象的`value`属性,就是紧跟在`return`语句后面的表达式的值(如果没有`return`语句,则`value`属性的值为`undefined``done`属性的值`true`,表示遍历已经结束。
第四次调用此时Generator函数已经运行完毕`next`方法返回对象的`value`属性为undefined`done`属性为true。以后再调用`next`方法,返回的都是这个值。 第四次调用,此时 Generator 函数已经运行完毕,`next`方法返回对象的`value`属性为`undefined``done`属性为`true`。以后再调用`next`方法,返回的都是这个值。
总结一下调用Generator函数返回一个遍历器对象代表Generator函数的内部指针。以后每次调用遍历器对象的`next`方法,就会返回一个有着`value``done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`语句后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。 总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的`next`方法,就会返回一个有着`value``done`两个属性的对象。`value`属性表示当前的内部状态的值,是`yield`表达式后面那个表达式的值;`done`属性是一个布尔值,表示是否遍历结束。
ES6没有规定`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。 ES6 没有规定,`function`关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
```javascript ```javascript
function * foo(x, y) { ··· } function * foo(x, y) { ··· }
function *foo(x, y) { ··· } function *foo(x, y) { ··· }
function* foo(x, y) { ··· } function* foo(x, y) { ··· }
function*foo(x, y) { ··· } function*foo(x, y) { ··· }
``` ```
由于Generator函数仍然是普通函数所以一般的写法是上面的第三种即星号紧跟在`function`关键字后面。本书也采用这种写法。 由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在`function`关键字后面。本书也采用这种写法。
### yield语句 ### yield 表达式
由于Generator函数返回的遍历器对象只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`语句就是暂停标志。 由于 Generator 函数返回的遍历器对象,只有调用`next`方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。`yield`表达式就是暂停标志。
遍历器对象的`next`方法的运行逻辑如下。 遍历器对象的`next`方法的运行逻辑如下。
1遇到`yield`语句,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。 1遇到`yield`表达式,就暂停执行后面的操作,并将紧跟在`yield`后面的那个表达式的值,作为返回的对象的`value`属性值。
2下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`语句 2下一次调用`next`方法时,再继续往下执行,直到遇到下一个`yield`表达式
3如果没有再遇到新的`yield`语句,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。 3如果没有再遇到新的`yield`表达式,就一直运行到函数结束,直到`return`语句为止,并将`return`语句后面的表达式的值,作为返回的对象的`value`属性值。
4如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined` 4如果该函数没有`return`语句,则返回的对象的`value`属性值为`undefined`
需要注意的是,`yield`语句后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行因此等于为JavaScript提供了手动的“惰性求值”Lazy Evaluation的语法功能。 需要注意的是,`yield`表达式后面的表达式,只有当调用`next`方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”Lazy Evaluation的语法功能。
```javascript ```javascript
function* gen() { function* gen() {
@ -90,11 +87,11 @@ function* gen() {
} }
``` ```
上面代码中yield后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。 上面代码中,`yield`后面的表达式`123 + 456`,不会立即求值,只会在`next`方法将指针移到这一句时,才会求值。
`yield`语句`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`语句。正常函数只能返回一个值,因为只能执行一次`return`Generator函数可以返回一系列的值因为可以有任意多个`yield`。从另一个角度看也可以说Generator生成了一系列的值这也就是它的名称的来历英语中generator这个词是“生成器”的意思 `yield`表达式`return`语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到`yield`,函数暂停执行,下一次再从该位置继续向后执行,而`return`语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)`return`语句,但是可以执行多次(或者说多个)`yield`表达式。正常函数只能返回一个值,因为只能执行一次`return`Generator 函数可以返回一系列的值,因为可以有任意多个`yield`。从另一个角度看,也可以说 Generator 生成了一系列的值这也就是它的名称的来历英语中generator 这个词是“生成器”的意思)。
Generator函数可以不用`yield`语句,这时就变成了一个单纯的暂缓执行函数。 Generator 函数可以不用`yield`表达式,这时就变成了一个单纯的暂缓执行函数。
```javascript ```javascript
function* f() { function* f() {
@ -110,7 +107,7 @@ setTimeout(function () {
上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。 上面代码中,函数`f`如果是普通函数,在为变量`generator`赋值时就会执行。但是,函数`f`是一个 Generator 函数,就变成只有调用`next`方法时,函数`f`才会执行。
另外需要注意,`yield`语句只能用在 Generator 函数里面,用在其他地方都会报错。 另外需要注意,`yield`表达式只能用在 Generator 函数里面,用在其他地方都会报错。
```javascript ```javascript
(function (){ (function (){
@ -119,7 +116,7 @@ setTimeout(function () {
// SyntaxError: Unexpected number // SyntaxError: Unexpected number
``` ```
上面代码在一个普通函数中使用`yield`语句,结果产生一个句法错误。 上面代码在一个普通函数中使用`yield`表达式,结果产生一个句法错误。
下面是另外一个例子。 下面是另外一个例子。
@ -141,7 +138,7 @@ for (var f of flat(arr)){
} }
``` ```
上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`语句(这个函数里面还使用了`yield*`语句,详细介绍见后文)。一种修改方法是改用`for`循环。 上面代码也会产生句法错误,因为`forEach`方法的参数是一个普通函数,但是在里面使用了`yield`表达式(这个函数里面还使用了`yield*`表达式,详细介绍见后文)。一种修改方法是改用`for`循环。
```javascript ```javascript
var arr = [1, [[2, 3], 4], [5, 6]]; var arr = [1, [[2, 3], 4], [5, 6]];
@ -164,7 +161,7 @@ for (var f of flat(arr)) {
// 1, 2, 3, 4, 5, 6 // 1, 2, 3, 4, 5, 6
``` ```
另外,`yield`语句如果用在一个表达式之中,必须放在圆括号里面。 另外,`yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。
```javascript ```javascript
function* demo() { function* demo() {
@ -176,7 +173,7 @@ function* demo() {
} }
``` ```
`yield`语句用作函数参数或放在赋值表达式的右边,可以不加括号。 `yield`表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
```javascript ```javascript
function* demo() { function* demo() {
@ -189,7 +186,7 @@ function* demo() {
上一章说过,任意一个对象的`Symbol.iterator`方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。 上一章说过,任意一个对象的`Symbol.iterator`方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于Generator函数就是遍历器生成函数因此可以把Generator赋值给对象的`Symbol.iterator`属性从而使得该对象具有Iterator接口。 由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的`Symbol.iterator`属性,从而使得该对象具有 Iterator 接口。
```javascript ```javascript
var myIterable = {}; var myIterable = {};
@ -202,9 +199,9 @@ myIterable[Symbol.iterator] = function* () {
[...myIterable] // [1, 2, 3] [...myIterable] // [1, 2, 3]
``` ```
上面代码中Generator函数赋值给`Symbol.iterator`属性,从而使得`myIterable`对象具有了Iterator接口可以被`...`运算符遍历了。 上面代码中Generator 函数赋值给`Symbol.iterator`属性,从而使得`myIterable`对象具有了 Iterator 接口,可以被`...`运算符遍历了。
Generator函数执行后返回一个遍历器对象。该对象本身也具有`Symbol.iterator`属性,执行后返回自身。 Generator 函数执行后,返回一个遍历器对象。该对象本身也具有`Symbol.iterator`属性,执行后返回自身。
```javascript ```javascript
function* gen(){ function* gen(){
@ -217,11 +214,11 @@ g[Symbol.iterator]() === g
// true // true
``` ```
上面代码中,`gen`是一个Generator函数调用它会生成一个遍历器对象`g`。它的`Symbol.iterator`属性,也是一个遍历器对象生成函数,执行后返回它自己。 上面代码中,`gen`是一个 Generator 函数,调用它会生成一个遍历器对象`g`。它的`Symbol.iterator`属性,也是一个遍历器对象生成函数,执行后返回它自己。
## next方法的参数 ## next 方法的参数
`yield`语句本身没有返回值,或者说总是返回`undefined``next`方法可以带一个参数,该参数就会被当作上一个`yield`语句的返回值。 `yield`表达式本身没有返回值,或者说总是返回`undefined``next`方法可以带一个参数,该参数就会被当作上一个`yield`表达式的返回值。
```javascript ```javascript
function* f() { function* f() {
@ -238,7 +235,7 @@ g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false } g.next(true) // { value: 0, done: false }
``` ```
上面代码先定义了一个可以无限运行的 Generator 函数`f`,如果`next`方法没有参数,每次运行到`yield`语句,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,变量`reset`就被重置为这个参数(即`true`),因此`i`会等于`-1`,下一轮循环就会从`-1`开始递增。 上面代码先定义了一个可以无限运行的 Generator 函数`f`,如果`next`方法没有参数,每次运行到`yield`表达式,变量`reset`的值总是`undefined`。当`next`方法带一个参数`true`时,变量`reset`就被重置为这个参数(即`true`),因此`i`会等于`-1`,下一轮循环就会从`-1`开始递增。
这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行它的上下文状态context是不变的。通过`next`方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。 这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行它的上下文状态context是不变的。通过`next`方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
@ -264,11 +261,11 @@ b.next(13) // { value:42, done:true }
上面代码中,第二次运行`next`方法的时候不带参数导致y的值等于`2 * undefined`(即`NaN`除以3以后还是`NaN`,因此返回对象的`value`属性也等于`NaN`。第三次运行`Next`方法的时候不带参数,所以`z`等于`undefined`,返回对象的`value`属性等于`5 + NaN + undefined`,即`NaN` 上面代码中,第二次运行`next`方法的时候不带参数导致y的值等于`2 * undefined`(即`NaN`除以3以后还是`NaN`,因此返回对象的`value`属性也等于`NaN`。第三次运行`Next`方法的时候不带参数,所以`z`等于`undefined`,返回对象的`value`属性等于`5 + NaN + undefined`,即`NaN`
如果向`next`方法提供参数,返回结果就完全不一样了。上面代码第一次调用`b``next`方法时,返回`x+1`的值6第二次调用`next`方法,将上一次`yield`语句的值设为12因此`y`等于24返回`y / 3`的值8第三次调用`next`方法,将上一次`yield`语句的值设为13因此`z`等于13这时`x`等于5`y`等于24所以`return`语句的值等于42 如果向`next`方法提供参数,返回结果就完全不一样了。上面代码第一次调用`b``next`方法时,返回`x+1`的值`6`;第二次调用`next`方法,将上一次`yield`表达式的值设为`12`,因此`y`等于`24`,返回`y / 3`的值`8`;第三次调用`next`方法,将上一次`yield`表达式的值设为`13`,因此`z`等于`13`,这时`x`等于`5``y`等于`24`,所以`return`语句的值等于`42`
注意,由于`next`方法的参数表示上一个`yield`语句的返回值,所以第一次使用`next`方法时不能带有参数。V8引擎直接忽略第一次使用`next`方法时的参数,只有从第二次使用`next`方法开始,参数才是有效的。从语义上讲,第一个`next`方法用来启动遍历器对象,所以不用带有参数。 注意,由于`next`方法的参数表示上一个`yield`表达式的返回值,所以第一次使用`next`方法时不能带有参数。V8 引擎直接忽略第一次使用`next`方法时的参数,只有从第二次使用`next`方法开始,参数才是有效的。从语义上讲,第一个`next`方法用来启动遍历器对象,所以不用带有参数。
如果想要第一次调用`next`方法时就能够输入值可以在Generator函数外面再包一层。 如果想要第一次调用`next`方法时,就能够输入值,可以在 Generator 函数外面再包一层。
```javascript ```javascript
function wrapper(generatorFunction) { function wrapper(generatorFunction) {
@ -288,9 +285,9 @@ wrapped().next('hello!')
// First input: hello! // First input: hello!
``` ```
上面代码中Generator函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。 上面代码中Generator 函数如果不用`wrapper`先包一层,是无法第一次调用`next`方法,就输入参数的。
再看一个通过`next`方法的参数向Generator函数内部输入值的例子。 再看一个通过`next`方法的参数,向 Generator 函数内部输入值的例子。
```javascript ```javascript
function* dataConsumer() { function* dataConsumer() {
@ -309,11 +306,11 @@ genObj.next('b')
// 2. b // 2. b
``` ```
上面代码是一个很直观的例子,每次通过`next`方法向Generator函数输入值然后打印出来。 上面代码是一个很直观的例子,每次通过`next`方法向 Generator 函数输入值,然后打印出来。
## for...of循环 ## for...of 循环
`for...of`循环可以自动遍历Generator函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。 `for...of`循环可以自动遍历 Generator 函数时生成的`Iterator`对象,且此时不再需要调用`next`方法。
```javascript ```javascript
function *foo() { function *foo() {
@ -331,9 +328,9 @@ for (let v of foo()) {
// 1 2 3 4 5 // 1 2 3 4 5
``` ```
上面代码使用`for...of`循环依次显示5个`yield`语句的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true``for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的6不包括在`for...of`循环之中。 上面代码使用`for...of`循环依次显示5个`yield`表达式的值。这里需要注意,一旦`next`方法的返回对象的`done`属性为`true``for...of`循环就会中止,且不包含该返回对象,所以上面代码的`return`语句返回的`6`,不包括在`for...of`循环之中。
下面是一个利用Generator函数和`for...of`循环,实现斐波那契数列的例子。 下面是一个利用 Generator 函数和`for...of`循环,实现斐波那契数列的例子。
```javascript ```javascript
function* fibonacci() { function* fibonacci() {
@ -352,7 +349,7 @@ for (let n of fibonacci()) {
从上面代码可见,使用`for...of`语句时不需要使用`next`方法。 从上面代码可见,使用`for...of`语句时不需要使用`next`方法。
利用`for...of`循环可以写出遍历任意对象object的方法。原生的JavaScript对象没有遍历接口无法使用`for...of`循环通过Generator函数为它加上这个接口就可以用了。 利用`for...of`循环可以写出遍历任意对象object的方法。原生的 JavaScript 对象没有遍历接口,无法使用`for...of`循环,通过 Generator 函数为它加上这个接口,就可以用了。
```javascript ```javascript
function* objectEntries(obj) { function* objectEntries(obj) {
@ -372,7 +369,7 @@ for (let [key, value] of objectEntries(jane)) {
// last: Doe // last: Doe
``` ```
上面代码中,对象`jane`原生不具备Iterator接口无法用`for...of`遍历。这时我们通过Generator函数`objectEntries`为它加上遍历器接口,就可以用`for...of`遍历了。加上遍历器接口的另一种写法是将Generator函数加到对象的`Symbol.iterator`属性上面。 上面代码中,对象`jane`原生不具备 Iterator 接口,无法用`for...of`遍历。这时,我们通过 Generator 函数`objectEntries`为它加上遍历器接口,就可以用`for...of`遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的`Symbol.iterator`属性上面。
```javascript ```javascript
function* objectEntries() { function* objectEntries() {
@ -394,7 +391,7 @@ for (let [key, value] of jane) {
// last: Doe // last: Doe
``` ```
除了`for...of`循环以外,扩展运算符(`...`)、解构赋值和`Array.from`方法内部调用的都是遍历器接口。这意味着它们都可以将Generator函数返回的Iterator对象作为参数。 除了`for...of`循环以外,扩展运算符(`...`)、解构赋值和`Array.from`方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
```javascript ```javascript
function* numbers () { function* numbers () {
@ -425,7 +422,7 @@ for (let n of numbers()) {
## Generator.prototype.throw() ## Generator.prototype.throw()
Generator函数返回的遍历器对象都有一个`throw`方法可以在函数体外抛出错误然后在Generator函数体内捕获。 Generator 函数返回的遍历器对象,都有一个`throw`方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
```javascript ```javascript
var g = function* () { var g = function* () {
@ -449,7 +446,7 @@ try {
// 外部捕获 b // 外部捕获 b
``` ```
上面代码中,遍历器对象`i`连续抛出两个错误。第一个错误被Generator函数体内的`catch`语句捕获。`i`第二次抛出错误由于Generator函数内部的`catch`语句已经执行过了不会再捕捉到这个错误了所以这个错误就被抛出了Generator函数体被函数体外的`catch`语句捕获。 上面代码中,遍历器对象`i`连续抛出两个错误。第一个错误被 Generator 函数体内的`catch`语句捕获。`i`第二次抛出错误,由于 Generator 函数内部的`catch`语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的`catch`语句捕获。
`throw`方法可以接受一个参数,该参数会被`catch`语句接收,建议抛出`Error`对象的实例。 `throw`方法可以接受一个参数,该参数会被`catch`语句接收,建议抛出`Error`对象的实例。
@ -496,7 +493,7 @@ try {
上面代码之所以只捕获了`a`,是因为函数体外的`catch`语句块,捕获了抛出的`a`错误以后,就不会再继续`try`代码块里面剩余的语句了。 上面代码之所以只捕获了`a`,是因为函数体外的`catch`语句块,捕获了抛出的`a`错误以后,就不会再继续`try`代码块里面剩余的语句了。
如果Generator函数内部没有部署`try...catch`代码块,那么`throw`方法抛出的错误,将被外部`try...catch`代码块捕获。 如果 Generator 函数内部没有部署`try...catch`代码块,那么`throw`方法抛出的错误,将被外部`try...catch`代码块捕获。
```javascript ```javascript
var g = function* () { var g = function* () {
@ -518,9 +515,9 @@ try {
// 外部捕获 a // 外部捕获 a
``` ```
上面代码中Generator函数`g`内部没有部署`try...catch`代码块,所以抛出的错误直接被外部`catch`代码块捕获。 上面代码中Generator 函数`g`内部没有部署`try...catch`代码块,所以抛出的错误直接被外部`catch`代码块捕获。
如果Generator函数内部和外部都没有部署`try...catch`代码块,那么程序将报错,直接中断执行。 如果 Generator 函数内部和外部,都没有部署`try...catch`代码块,那么程序将报错,直接中断执行。
```javascript ```javascript
var gen = function* gen(){ var gen = function* gen(){
@ -537,7 +534,7 @@ g.throw();
上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。 上面代码中,`g.throw`抛出错误以后,没有任何`try...catch`代码块可以捕获这个错误,导致程序报错,中断执行。
`throw`方法被捕获以后,会附带执行下一条`yield`语句。也就是说,会附带执行一次`next`方法。 `throw`方法被捕获以后,会附带执行下一条`yield`表达式。也就是说,会附带执行一次`next`方法。
```javascript ```javascript
var gen = function* gen(){ var gen = function* gen(){
@ -556,7 +553,7 @@ g.throw() // b
g.next() // c g.next() // c
``` ```
上面代码中,`g.throw`方法被捕获以后,自动执行了一次`next`方法,所以会打印`b`。另外也可以看到只要Generator函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。 上面代码中,`g.throw`方法被捕获以后,自动执行了一次`next`方法,所以会打印`b`。另外,也可以看到,只要 Generator 函数内部部署了`try...catch`代码块,那么遍历器的`throw`方法抛出的错误,不影响下一次遍历。
另外,`throw`命令与`g.throw`方法是无关的,两者互不影响。 另外,`throw`命令与`g.throw`方法是无关的,两者互不影响。
@ -580,9 +577,9 @@ try {
上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。 上面代码中,`throw`命令抛出的错误不会影响到遍历器的状态,所以两次执行`next`方法,都进行了正确的操作。
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`语句,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法想要捕获多个错误就不得不为每个函数内部写一个错误处理语句现在只在Generator函数内部写一次`catch`语句就可以了。 这种函数体内捕获错误的机制,大大方便了对错误的处理。多个`yield`表达式,可以只用一个`try...catch`代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次`catch`语句就可以了。
Generator函数体外抛出的错误可以在函数体内捕获反过来Generator函数体内抛出的错误也可以被函数体外的`catch`捕获。 Generator 函数体外抛出的错误可以在函数体内捕获反过来Generator 函数体内抛出的错误,也可以被函数体外的`catch`捕获。
```javascript ```javascript
function* foo() { function* foo() {
@ -604,7 +601,7 @@ try {
上面代码中,第二个`next`方法向函数体内传入一个参数42数值是没有`toUpperCase`方法的所以会抛出一个TypeError错误被函数体外的`catch`捕获。 上面代码中,第二个`next`方法向函数体内传入一个参数42数值是没有`toUpperCase`方法的所以会抛出一个TypeError错误被函数体外的`catch`捕获。
一旦Generator执行过程中抛出错误且没有被内部捕获就不会再执行下去了。如果此后还调用`next`方法,将返回一个`value`属性等于`undefined``done`属性等于`true`的对象即JavaScript引擎认为这个Generator已经运行结束了。 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用`next`方法,将返回一个`value`属性等于`undefined``done`属性等于`true`的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
```javascript ```javascript
function* g() { function* g() {
@ -648,7 +645,7 @@ log(g());
// caller done // caller done
``` ```
上面代码一共三次运行`next`方法第二次运行的时候会抛出错误然后第三次运行的时候Generator函数就已经结束了不再执行下去了。 上面代码一共三次运行`next`方法第二次运行的时候会抛出错误然后第三次运行的时候Generator 函数就已经结束了,不再执行下去了。
## Generator.prototype.return() ## Generator.prototype.return()
@ -685,7 +682,7 @@ g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true } g.return() // { value: undefined, done: true }
``` ```
如果Generator函数内部有`try...finally`代码块,那么`return`方法会推迟到`finally`代码块执行完再执行。 如果 Generator 函数内部有`try...finally`代码块,那么`return`方法会推迟到`finally`代码块执行完再执行。
```javascript ```javascript
function* numbers () { function* numbers () {
@ -709,7 +706,7 @@ g.next() // { value: 7, done: true }
上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。 上面代码中,调用`return`方法后,就开始执行`finally`代码块,然后等到`finally`代码块执行完,再执行`return`方法。
## yield* 语句 ## yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。 如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
@ -734,7 +731,7 @@ for (let v of bar()){
上面代码中,`foo``bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。 上面代码中,`foo``bar`都是 Generator 函数,在`bar`里面调用`foo`,是不会有效果的。
这个就需要用到`yield*`语句,用来在一个 Generator 函数里面执行另一个 Generator 函数。 这个就需要用到`yield*`表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
```javascript ```javascript
function* bar() { function* bar() {
@ -801,7 +798,7 @@ gen.next().value // "close"
上面例子中,`outer2`使用了`yield*``outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。 上面例子中,`outer2`使用了`yield*``outer1`没使用。结果就是,`outer1`返回一个遍历器对象,`outer2`返回该遍历器对象的内部值。
从语法角度看,如果`yield`命令后面跟的是一个遍历器对象,需要在`yield`命令后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`语句 从语法角度看,如果`yield`表达式后面跟的是一个遍历器对象,需要在`yield`表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为`yield*`表达式
```javascript ```javascript
let delegatedIterator = (function* () { let delegatedIterator = (function* () {
@ -826,7 +823,7 @@ for(let value of delegatingIterator) {
上面代码中,`delegatingIterator`是代理者,`delegatedIterator`是被代理者。由于`yield* delegatedIterator`语句得到的值是一个遍历器所以要用星号表示。运行结果就是使用一个遍历器遍历了多个Generator函数有递归的效果。 上面代码中,`delegatingIterator`是代理者,`delegatedIterator`是被代理者。由于`yield* delegatedIterator`语句得到的值是一个遍历器所以要用星号表示。运行结果就是使用一个遍历器遍历了多个Generator函数有递归的效果。
`yield*`后面的Generator函数没有`return`语句时等同于在Generator函数内部部署一个`for...of`循环。 `yield*`后面的 Generator 函数(没有`return`语句时),等同于在 Generator 函数内部,部署一个`for...of`循环。
```javascript ```javascript
function* concat(iter1, iter2) { function* concat(iter1, iter2) {
@ -860,7 +857,7 @@ gen().next() // { value:"a", done:false }
上面代码中,`yield`命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。 上面代码中,`yield`命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
实际上任何数据结构只要有Iterator接口就可以被`yield*`遍历。 实际上,任何数据结构只要有 Iterator 接口,就可以被`yield*`遍历。
```javascript ```javascript
let read = (function* () { let read = (function* () {
@ -872,9 +869,9 @@ read.next().value // "hello"
read.next().value // "h" read.next().value // "h"
``` ```
上面代码中,`yield`语句返回整个字符串,`yield*`语句返回单个字符。因为字符串具有Iterator接口所以被`yield*`遍历。 上面代码中,`yield`表达式返回整个字符串,`yield*`语句返回单个字符。因为字符串具有 Iterator 接口,所以被`yield*`遍历。
如果被代理的Generator函数有`return`语句那么就可以向代理它的Generator函数返回数据。 如果被代理的 Generator 函数有`return`语句,那么就可以向代理它的 Generator 函数返回数据。
```javascript ```javascript
function *foo() { function *foo() {
@ -994,7 +991,7 @@ result
## 作为对象属性的Generator函数 ## 作为对象属性的Generator函数
如果一个对象的属性是Generator函数可以简写成下面的形式。 如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
```javascript ```javascript
let obj = { let obj = {
@ -1004,7 +1001,7 @@ let obj = {
}; };
``` ```
上面代码中,`myGeneratorMethod`属性前面有一个星号表示这个属性是一个Generator函数。 上面代码中,`myGeneratorMethod`属性前面有一个星号,表示这个属性是一个 Generator 函数。
它的完整形式如下,与上面的写法是等价的。 它的完整形式如下,与上面的写法是等价的。
@ -1016,9 +1013,9 @@ let obj = {
}; };
``` ```
## Generator函数的`this` ## Generator 函数的`this`
Generator函数总是返回一个遍历器ES6规定这个遍历器是Generator函数的实例也继承了Generator函数的`prototype`对象上的方法。 Generator 函数总是返回一个遍历器ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的`prototype`对象上的方法。
```javascript ```javascript
function* g() {} function* g() {}
@ -1033,7 +1030,7 @@ obj instanceof g // true
obj.hello() // 'hi!' obj.hello() // 'hi!'
``` ```
上面代码表明Generator函数`g`返回的遍历器`obj`,是`g`的实例,而且继承了`g.prototype`。但是,如果把`g`当作普通的构造函数,并不会生效,因为`g`返回的总是遍历器对象,而不是`this`对象。 上面代码表明Generator 函数`g`返回的遍历器`obj`,是`g`的实例,而且继承了`g.prototype`。但是,如果把`g`当作普通的构造函数,并不会生效,因为`g`返回的总是遍历器对象,而不是`this`对象。
```javascript ```javascript
function* g() { function* g() {
@ -1060,9 +1057,9 @@ new F()
上面代码中,`new`命令跟构造函数`F`一起使用,结果报错,因为`F`不是构造函数。 上面代码中,`new`命令跟构造函数`F`一起使用,结果报错,因为`F`不是构造函数。
那么有没有办法让Generator函数返回一个正常的对象实例既可以用`next`方法,又可以获得正常的`this` 那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用`next`方法,又可以获得正常的`this`
下面是一个变通方法。首先,生成一个空对象,使用`call`方法绑定Generator函数内部的`this`。这样构造函数调用以后这个空对象就是Generator函数的实例对象了。 下面是一个变通方法。首先,生成一个空对象,使用`call`方法绑定 Generator 函数内部的`this`。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
```javascript ```javascript
function* F() { function* F() {
@ -1082,7 +1079,7 @@ obj.b // 2
obj.c // 3 obj.c // 3
``` ```
上面代码中,首先是`F`内部的`this`对象绑定`obj`对象然后调用它返回一个Iterator对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`语句完成F内部所有代码的运行。这时所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。 上面代码中,首先是`F`内部的`this`对象绑定`obj`对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次`next`方法(因为`F`内部有两个`yield`表达式完成F内部所有代码的运行。这时所有内部属性都绑定在`obj`对象上了,因此`obj`对象也就成了`F`的实例。
上面代码中,执行的是遍历器对象`f`,但是生成的对象实例是`obj`,有没有办法将这两个对象统一呢? 上面代码中,执行的是遍历器对象`f`,但是生成的对象实例是`obj`,有没有办法将这两个对象统一呢?
@ -1131,9 +1128,9 @@ f.c // 3
## 含义 ## 含义
### Generator与状态机 ### Generator 与状态机
Generator是实现状态机的最佳结构。比如下面的clock函数就是一个状态机。 Generator 是实现状态机的最佳结构。比如,下面的`clock`函数就是一个状态机。
```javascript ```javascript
var ticking = true; var ticking = true;
@ -1146,10 +1143,10 @@ var clock = function() {
} }
``` ```
上面代码的clock函数一共有两种状态Tick和Tock每运行一次就改变一次状态。这个函数如果用Generator实现就是下面这样。 上面代码的`clock`函数一共有两种状态(`Tick``Tock`),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
```javascript ```javascript
var clock = function*() { var clock = function* () {
while (true) { while (true) {
console.log('Tick!'); console.log('Tick!');
yield; yield;
@ -1159,7 +1156,7 @@ var clock = function*() {
}; };
``` ```
上面的Generator实现与ES5实现对比可以看到少了用来保存状态的外部变量`ticking`这样就更简洁更安全状态不会被非法篡改、更符合函数式编程的思想在写法上也更优雅。Generator之所以可以不用外部变量保存状态是因为它本身就包含了一个状态信息即目前是否处于暂停态。 上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量`ticking`这样就更简洁更安全状态不会被非法篡改、更符合函数式编程的思想在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
### Generator与协程 ### Generator与协程
@ -1175,19 +1172,19 @@ var clock = function*() {
不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。 不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。
由于ECMAScript是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。 由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator函数是ECMAScript 6对协程的实现但属于不完全实现。Generator函数被称为“半协程”semi-coroutine意思是只有Generator函数的调用者才能将程序的执行权还给Generator函数。如果是完全执行的协程任何函数都可以让暂停的协程继续执行。 Generator 函数是 ES6 对协程的实现但属于不完全实现。Generator 函数被称为“半协程”semi-coroutine意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
如果将Generator函数当作协程完全可以将多个需要互相协作的任务写成Generator函数它们之间使用yield语句交换控制权。 如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用`yield`表示式交换控制权。
## 应用 ## 应用
Generator可以暂停函数执行返回任意表达式的值。这种特点使得Generator有多种应用场景。 Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
### 1异步操作的同步化表达 ### 1异步操作的同步化表达
Generator函数的暂停执行的效果意味着可以把异步操作写在yield语句里面等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield语句下面反正要等到调用next方法时再执行。所以Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。 Generator 函数的暂停执行的效果,意味着可以把异步操作写在`yield`表达式里面,等到调用`next`方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在`yield`表达式下面,反正要等到调用`next`方法时再执行。所以Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
```javascript ```javascript
function* loadUI() { function* loadUI() {
@ -1203,9 +1200,9 @@ loader.next()
loader.next() loader.next()
``` ```
上面代码表示第一次调用loadUI函数时该函数不会执行仅返回一个遍历器。下一次对该遍历器调用next方法则会显示Loading界面并且异步加载数据。等到数据加载完成再一次使用next方法则会隐藏Loading界面。可以看到这种写法的好处是所有Loading界面的逻辑都被封装在一个函数按部就班非常清晰。 上面代码中,第一次调用`loadUI`函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用`next`方法,则会显示`Loading`界面`showLoadingScreen`,并且异步加载数据`loadUIDataAsynchronously`。等到数据加载完成,再一次使用`next`方法,则会隐藏`Loading`界面。可以看到,这种写法的好处是所有`Loading`界面的逻辑,都被封装在一个函数,按部就班非常清晰。
Ajax是典型的异步操作通过Generator函数部署Ajax操作可以用同步的方式表达。 Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
```javascript ```javascript
function* main() { function* main() {
@ -1224,9 +1221,9 @@ var it = main();
it.next(); it.next();
``` ```
上面代码的main函数就是通过Ajax操作获取数据。可以看到除了多了一个yield它几乎与同步操作的写法完全一样。注意makeAjaxCall函数中的next方法必须加上response参数因为yield语句构成的表达式本身是没有值的总是等于undefined 上面代码的`main`函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个`yield`,它几乎与同步操作的写法完全一样。注意,`makeAjaxCall`函数中的`next`方法,必须加上`response`参数,因为`yield`表达式,本身是没有值的,总是等于`undefined`
下面是另一个例子通过Generator函数逐行读取文本文件。 下面是另一个例子,通过 Generator 函数逐行读取文本文件。
```javascript ```javascript
function* numbers() { function* numbers() {
@ -1241,7 +1238,7 @@ function* numbers() {
} }
``` ```
上面代码打开文本文件,使用yield语句可以手动逐行读取文件。 上面代码打开文本文件,使用`yield`表达式可以手动逐行读取文件。
### 2控制流管理 ### 2控制流管理
@ -1259,7 +1256,7 @@ step1(function (value1) {
}); });
``` ```
采用Promise改写上面的代码。 采用 Promise 改写上面的代码。
```javascript ```javascript
Promise.resolve(step1) Promise.resolve(step1)
@ -1274,7 +1271,7 @@ Promise.resolve(step1)
.done(); .done();
``` ```
上面代码已经把回调函数改成了直线执行的形式但是加入了大量Promise的语法。Generator函数可以进一步改善代码运行流程。 上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
```javascript ```javascript
function* longRunningTask(value1) { function* longRunningTask(value1) {
@ -1320,22 +1317,22 @@ function *iterateSteps(steps){
} }
``` ```
上面代码中,数组`steps`封装了一个任务的多个步骤Generator函数`iterateSteps`则是依次为这些步骤加上`yield`命令。 上面代码中,数组`steps`封装了一个任务的多个步骤Generator 函数`iterateSteps`则是依次为这些步骤加上`yield`命令。
将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。 将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
```javascript ```javascript
let jobs = [job1, job2, job3]; let jobs = [job1, job2, job3];
function *iterateJobs(jobs){ function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){ for (var i=0; i< jobs.length; i++){
var job = jobs[i]; var job = jobs[i];
yield *iterateSteps(job.steps); yield* iterateSteps(job.steps);
} }
} }
``` ```
上面代码中,数组`jobs`封装了一个项目的多个任务Generator函数`iterateJobs`则是依次为这些任务加上`yield *`命令。 上面代码中,数组`jobs`封装了一个项目的多个任务Generator 函数`iterateJobs`则是依次为这些任务加上`yield*`命令。
最后,就可以用`for...of`循环一次性依次执行所有任务的所有步骤。 最后,就可以用`for...of`循环一次性依次执行所有任务的所有步骤。
@ -1360,9 +1357,9 @@ while (!res.done){
} }
``` ```
### 3部署Iterator接口 ### 3部署 Iterator 接口
利用Generator函数可以在任意对象上部署Iterator接口。 利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
```javascript ```javascript
function* iterEntries(obj) { function* iterEntries(obj) {
@ -1383,9 +1380,9 @@ for (let [key, value] of iterEntries(myObj)) {
// bar 7 // bar 7
``` ```
上述代码中,`myObj`是一个普通对象,通过`iterEntries`函数就有了Iterator接口。也就是说可以在任意对象上部署`next`方法。 上述代码中,`myObj`是一个普通对象,通过`iterEntries`函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署`next`方法。
下面是一个对数组部署Iterator接口的例子尽管数组原生具有这个接口。 下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。
```javascript ```javascript
function* makeSimpleGenerator(array){ function* makeSimpleGenerator(array){
@ -1405,7 +1402,7 @@ gen.next().done // true
### 4作为数据结构 ### 4作为数据结构
Generator可以看作是数据结构更确切地说可以看作是一个数组结构因为Generator函数可以返回一系列的值这意味着它可以对任意表达式提供类似数组的接口。 Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
```javascript ```javascript
function *doStuff() { function *doStuff() {
@ -1415,7 +1412,7 @@ function *doStuff() {
} }
``` ```
上面代码就是依次返回三个函数但是由于使用了Generator函数导致可以像处理数组那样处理这三个返回的函数。 上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。
```javascript ```javascript
for (task of doStuff()) { for (task of doStuff()) {
@ -1423,7 +1420,7 @@ for (task of doStuff()) {
} }
``` ```
实际上如果用ES5表达完全可以用数组模拟Generator的这种用法。 实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。
```javascript ```javascript
function doStuff() { function doStuff() {
@ -1435,5 +1432,5 @@ function doStuff() {
} }
``` ```
上面的函数可以用一模一样的for...of循环处理两相一比较就不难看出Generator使得数据或者操作具备了类似数组的接口。 上面的函数,可以用一模一样的`for...of`循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。