一、什么是生成器 Generator?
生成器对象是由一个 Generator 函数返回的,并且她符合 可迭代协议和迭代器协议。
语法:
function * gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen(); // "Generator { }"
生成器函数有以下四个特征:
- 在 function 关键字和函数名之间多了一个星号
- 函数内部使用了 yield 表达式,用于定义 Generator 函数中的每个状态
- Generator 函数通过多个 yield 表达式定义内部状态,掉用 Generator 函数时,不会像普通函数会立即执行,而是返回的是一个 Iterator 对象,通过调用 next() 方法,可以依次遍历 Generator 函数的每个内部状态
- Generator 函数的星号的位置有四种情况 如下
function *gen () {}
function* gen () {}
function * gen () {}
function*gen () {}
一般的写法是第二种。
二、如何使用 Generator?
1、yield 表达式
yield 关键字在 Generator 函数中有两个作用:定义内部状态和暂停执行,代码解释如下:
function *gen () {
yield 1
yield 2
return 3
}
const g = gen() // Iterator对象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
结合上述代码和第一节的Generator 函数的特征可知:
在调用 gen 函数时,返回的一个 Iterator 对象,要想获得每个 yield 表达式的状态,需要调用 next 方法。
每次调用 next 方法时,都会返回一个包含 value 属性和 done 属性的对象,value 属性表示 yield 表达式的值,done 属性是一个布尔值,表示遍历是否结束。
如果 value 没有返回值或者 返回的是 undefined ,说明是函数运行结束了。
还有一种情况需要注意的额是:yield 表达式 如果用在另一个表达式中,需要为其加上圆括号“()”,作为函数参数和语句时可以不适用圆括号
function *gen () {
console.log('hello' + yield) ×
console.log('hello' + (yield)) √
console.log('hello' + yield '凯斯') ×
console.log('hello' + (yield '凯斯')) √
foo(yield 1) √
const param = yield 2 √
}
2、yield* 表达式
yield* 表达式的使用场景:在一个 Generator 函数中调用另一个 Generator 函数。下面代码是不能执行的:
function *foo () {
yield 1
}
function *gen () {
foo()
yield 2
}
const g = gen()
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
如果使用 yield 表达式,value 返回的值是一个遍历器对象:
function *foo () {
yield 1
}
function *gen () {
yield foo()
yield 2
}
const g = gen()
g.next() // {value: Generator, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
这里要实现上面场景,要使用 yield* 表达式,其实这个表达式就是 for...of 方法的简写:
function *foo () {
yield 1
}
function *gen () {
yield* foo()
yield 2
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}
// 相当于
function *gen () {
yield 1
yield 2
}
// 相当于
function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}
yield* 遍历具有 Iterator 接口的数据类型:
const arr = ['a', 'b']
const str = 'yuan'
function *gen () {
yield arr
yield* arr
yield str
yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'yuan', done: false}
g.next() // {value: 'y', done: false}
...
使用 yield* 表达式取出嵌套数组中的成员:
// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
console.log(+item) // 1, 2, 3, 4
}
// 使用yield*表达式
function *gen (arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield * gen(arr[i])
}
} else {
yield arr
}
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
console.log(item) // 1, 2, 3, 4
}
3、next 方法
next 方法的作用是分阶段执行 Generator 函数。
每一次调用next方法,就会从函数头部或者上一次停下来的地方开始执行,直到遇到下一个yield表达式(return 语句)为止。同时,调用next方法时,会返回包含value和done属性的对象,value属性值可以为yield表达式、return语句后面的值或者undefined值,done属性表示遍历是否结束。
遍历器对象 next 方法的执行逻辑如下:
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield表达式后面的那个表达式的值,作为返回的对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
- 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到遇到return语句为止,并将return语句后面表达式的值,作为返回的对象的value属性值。
- 如果该函数没有return语句,则返回的对象的value属性值为undefined。
function *gen () {
yield 1
yield 2
return 3
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}
根据next运行逻辑再针对这个例子,就很容易理解了。调用gen函数,返回遍历器对象。
第一次调用next方法时,在遇到第一个yield表达式时停止执行,value属性值为1,即yield表达式后面的值,done为false表示遍历没有结束;
第二次调用next方法时,从暂停的yield表达式后开始执行,直到遇到下一个yield表达式后暂停执行,value属性值为2,done为false;
第三次调用next方法时,从上一次暂停的yield表达式后开始执行,由于后面没有yield表达式了,所以遇到return语句时函数执行结束,value属性值为return语句后面的值,done属性值为true表示已经遍历完毕了。
第四次调用next方法时,value属性值就是undefined了,此时done属性为true表示遍历完毕。以后再调用next方法都会是这两个值。
4、next 方法的参数
yield 语句本身没有返回值,或者说总是返回 undefined。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next() // {value: [undefined, NaN], done: true}
由上,第二次调用 next 方法时,并没有返回相应的值,而是返回 undefined,解决方法:给第二次调用的 next 方法传入一个参数,该参数会当作上一个 yield 语句的返回值。
function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next(10) // {value: [10, 5], done: true}```
注意,这里的 next 方法的参数是指上一个 yield 语句的返回值,所以在第一次调用 next 不能传入参数。如若要传参数,需要借用闭包来实现。
function wrapper (gen) {
return function (...args) {
const genObj = gen(...args)
genObj.next()
return genObj
}
}
const generator = wrapper(function *generator () {
console.log(`hello ${yield}`)
return 'done'
})
const a = generator().next('keith')
console.log(a) // hello keith, done
实际上,yield 表达式和 next 方法是 generator 函数的双向信息传递。yield 表达式向外传递 value 值,next 方法的参数向内传递值。
5、遍历 Generator 对象
与 Iterator 接口的关系
任何一个对象的Symbol.iterator属性,指向默认的遍历器对象生成函数。而Generator函数也是遍历器对象生成函数,所以可以将Generator函数赋值给Symbol.iterator属性,这样就使对象具有了Iterator接口。默认情况下,对象是没有Iterator接口的。
具有Iterator接口的对象,就可以被扩展运算符(...),解构赋值,Array.from和for...of循环遍历了。
function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}
for...of 循环可以自动遍历Generator函数生成的Iterator对象,不用调用next方法。
三、应用实例
1、协程
所谓协程:是指多个线程相互协作,完成异步任务。
Generator 函数是协程在 ES6 中的实现,最大特点就是可以交出函数的执行权。
整个Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方都用 yield 语句注明。Generator 函数实现协程代码如下:
function* gen() {
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next(); // { value: 3, done: false };
g.next(); // { value: undefined; done: true }
2、Thunk 函数
Thunk 函数是自动执行 Generator 函数的一种方法。
Thunk 函数的含义:起源于“传名调用”的“求职策略”。将参数放到一个临时函数之中,在将这个临时参数传入函数体。这个临时函数就是 Thunk 函数。
在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
任何函数,只要参数有回调函数,都能写成 Thenk 函数的形式。
Thunk 函数转换器:
// ES5 版本
var Thunk = function(fn) {
return function () {
var args = Array.prototype.slice.call(arguments);
return function (callback) {
args.push(callback);
return fn.apply(this, arg);
}
}
};
// ES6 版本
const Thunk = function(fn) {
return function(...args) {
return function(callback) {
return fn.call(this, ...args, callback);
}
}
}
使用上面的转换器,生成, fs.redFile 的 Thunk 函数。
var readFileThunk = Thunk(fs.readFile);
redadFileThunk(fileA)(callback);`
Thunk 函数真正的威力在于可以自动执行 Generator 函数。
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if(result.done) return;
result.value(next);
}
next();
}
function* g() {
...
}
run(g);
上面是一个基于 Thunk 函数的 Generator 执行器,可以在generator 函数内部执行多个异步操作。但是,这个里的每一个异步操作都必须是 Thunk 函数,也就是说,跟在 yield 表达式后面的必须是 Thunk 函数。
3、Co 模块
co 模块是用于 Generator 函数自动执行的一个小工具。
co 模块是讲两种自动执行器(Thunk 函数和 Promise 对象)包装成一个模块。使用 co 模块的前提条件是,Generator 函数的 yield 名利后面只是 Thunk 函数或 Promise 对象。
使用 co 模块处理并发的异步操作 :
1、并发操作放在数组里
co(function* () {
var res = yield [
Promise.resolve(1),
Promise.resolve(2)
];
console.log(res);
}).catch(onerror);
2、并发操作写在对象里
co(function* () {
var res = yield {
1: Promise.resolve(1),
2: Promise.resolve(2)
};
console.log(res);
}).catch(onerror);
3、处理多个 generator 异步操作
co(function* () {
var valuse = [n1, n2, n3];
yield values.map(somethingAsync);
});
function* somethingAsync(x) {
// do something async
return y;
}
上面代码允许并发 3 个 somethingAsync 异步操作。
四、结语
本章需要了解的是生成器函数 Generator 的具体含义及作用,如何使用 Generator 函数,以及 Generator 函数的几个特殊的应用实例。
章节目录
1、ES6中啥是块级作用域?运用在哪些地方?
2、ES6中使用解构赋值能带给我们什么?
3、ES6字符串扩展增加了哪些?
4、ES6对正则做了哪些扩展?
5、ES6数值多了哪些扩展?
6、ES6函数扩展(箭头函数)
7、ES6 数组给我们带来哪些操作便利?
8、ES6 对象扩展
9、Symbol 数据类型在 ES6 中起什么作用?
10、Map 和 Set 两数据结构在ES6的作用
11、ES6 中的Proxy 和 Reflect 到底是什么鬼?
12、从 Promise 开始踏入异步操作之旅
13、ES6 迭代器(Iterator)和 for...of循环使用方法
14、ES6 异步进阶第二步:Generator 函数
15、JavaScript 异步操作进阶第三步:async 函数
16、ES6 构造函数语法糖:class 类