在Javascript中,普通函数一旦开始运行在函数运行结束之前是不会中断的,而ES6引入的Generator
(生成器)可以使得函数可以发生中断,分步运行。
1 Iterable和Iterator
在说明如何创建和使用Generator
之前,先要说到ES6中迭代相关问题。
1.1 Iterable
在ES6中,可以使用for of
的方式来循环遍历对象:
for(let i of [1,2,3,4]){
console.log(i);
}
一个对象如果能够进行这样的迭代循环就被称为iterable
(可迭代)。在iterable
的对象中包含一个属性[Symbol.iterator]
,这个属性的值是一个function
,而这个function
是一个iterator
(迭代器)
var obj = {
[Symbol.iterator]:function iterator(){
// TODO 省略掉了iterator的实现
}
}
1.2 Iterator
一个iterator
是返回指一个包含next
属性,其值为是一个方法,且调用该next
方法会返回对象符合{value:xx, done: xx}
结构的值。
var iterator = function() {
let i = 0;
return {
next() {
i++;
return {value:i, done: i > 3}
}
}
}
1.3 ES6中的迭代
自定义iterable
对象的完整创建过程如下:
var o = {
[Symbol.iterator]: function() {
let i = 0;
return {
next() {
i++;
return {value:i, done: i > 3}
}
}
}
}
在使用for of
进行迭代操作的时候,每一次循环都会调用iterator
的next
函数,将每一次next
调用返回的value
作为迭代的值,同时会一直迭代直到done
的值返回true
为止
for(let i of o) {
console.log(i); // 1, 2, 3 因为当i>3的时候done返回为true,所以就停止执行
}
同时ES6中引入了[...x]
(分离操作)的语法糖,可以将iterable
对象迭代的结果值放到一个数组中:
var arr = [...o];
console.log(arr); // [1,2,3]
2. Generator的创建
生成器自身是iterable
的,同时自身是一个iterator
对象
2.1 使用function *()创建
生成器通常使用function *()
的方式进行创建,声明中*
的位置可以直接跟在function
关键字后面也可以在函数名前面,创建的生成器函数中包含yield
关键字。
function *g() { // 也可以是function* g()
yield 10;
}
2.2 使用GeneratorFunction构造方法创建
也可以使用GeneratorFunction
构造方法的形式创建生成器,不过用起来就比较复杂了,其中GeneratorFunction(...arguments, exec)
接受的最后一个参数exec
为生成器方法体,前面的参数为函数构造器的参数
var GeneratorFunction = Object.getPrototypeOf(function *(){}).constructor;
var g = new GeneratorFunction('a', 'yield a');
// 上面的构造过程相当于
function *g(a) {
yield a;
}
通常使用function *()
声明,因为看起来更适合阅读。
3 Generator的使用
3.1 获取iterator
在使用生成器的时候,直接调用构造方法,就可以返回一个iterator
(迭代器)
function *g() {
yield 1;
}
var it = g(); // 生成一个iterator
之前说过Generator
其自身是iterable
的同时是iterator
,主要表现在:
function *g(){
yield 1;
}
var it = g();
console.log(it[Symbol.iterator]() === it);
3.2 顺序化异步执行
构造了迭代器对象iterator
后,和普通的函数不同,生成器函数并没有进行执行,只有在调用了next
方法(也可以是return
或者throw
)方法以后,才会开始执行,且按照以下方式可以判断执行结果:
- 执行到第一个
yiled
停止,返回{value: x, done: x}
格式的数据,其中将yield
关键字后的值作为value
的值输出,并将done
设置为false
,再次调用next
方法,函数会再次向后执行- 如果调用
next
方法以后函数向后执行不存在yield
,则此时value
为undefined
(或者return
的值),done
将设置为true
- 如果调用
next
方法以后函数向后执行存在return关键字,则此时value
为return
的值,done
将设置为true
// 生成器最后不返回值
function *g1() {
var a = yield 3;
console.log(a);
}
var it1 = g1();
console.log(it1.next()); // {value: 3, done: false};
console.log(it1.next()); // {value: undefined, done: true};
// 生成器有return语句
function *g2() {
var a = yield 3;
console.log(a);
return 2;
}
var it2 = g2();
console.log(it2.next()); // {value: 3, done: false};
console.log(it2.next()); // {value: 2, done: true};
除了通过next
方法可以获取到yield
之后到值以外,next
方法可以添加一个参数,参数将把yield
表达式替换为该参数值
function *g1() {
var a = yield 3;
console.log('a=' + a);
}
var it1 = g1();
it1.next(); // 运行到`yield 3`停止,并返回{value: 3, done: false};
it1.next(1); // 继续运行,同时把`yield 3`替换为1,输出生成器函数中console.log('a=' + a)语句
要注意三点:
如果调用
return
,迭代器会立即结束,value
值为return
的值,done
设置为true
function *g(){
yield 1;
yield 2;
}
var it = g();
console.log(it.return(3)); // {value: 3, done: true};
console.log(it.next()); // {value: undefined, done: true}
如果生成器方法中存在
return
语句,执行到return
语句后会将返回值作为value
的值,且done
会变为true
,继续调用next
方法,不能获取到yield
的值,value
始终为undefined
,done
始终为true
function *g(){
yield 1;
return 2;
yield 3;
return 4;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // {value: 2, done: true};
console.log(it.next()); // {value: undefined, done: true};
console.log(it.next()); // {value: undefined, done: true};
如果调用迭代器的
throw
方法或者生成器函数中存在throw
操作,将抛出异常,迭代器运行结束
function *g() {
yield 1;
throw 2;
}
var it = g();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // 提示异常,并终止运行
// 或者
function *g() {
yield 1;
yield 2;
}
var it = g();
console.log(it.throw(2)); // 提示异常,并终止运行
console.log(it.next()); // 不会执行
3.2.1 在异步方法中的使用
生成器因为可以进行中断运行,且利用next
方法控制运行,所以对于异步方法,就可以使用顺序的方式来编写代码,而不用在回调中进行数据的处理。
// 普通方法执行
function f() {
var a;
setTimeout(_=> {
a = 3;
}, 1000);
var b = a * 100;
console.log('b=' + b);
}
f(); // 输出b=NaN
// 使用生成器执行
function *g() {
var a = yield setTimeout(_=> {
it.next(3);
}, 1000);
var b = a * 100; //
console.log('b=' + b);
}
var it = g();
it.next(); // 1s后会输出b=300
3.2.2 和Promise的联合用法
和在异步方法中的控制过程一样,可以在Promise
中使用来控制程序的执行
function *g() {
var p = yield Promise.resolve(3).then(fulfill => {
setTimeout(_=>{
it.next(fulfill);
}, 1000)
});
console.log('p=' + p);
}
var it = g();
it.next(); // 1s后输出 p=3
3.3 [Symbol.iterator]的迭代器
由于生成器的特性,于是用来创建iterable
对象的iterator
var obj = {
[Symbol.iterator]: function *g() {
let i = 0;
while(i < 3) {
i++;
yield i;
}
return 'finish' + i;
}
}
for(let o of obj) {
console.log(o); // 1, 2, 3
}
4 其他
4.1 形实转换程序
在Javascript中使用一个函数来来封装其他函数,并将需要的参数也一并封装
function f(x, y){
return x + y;
}
function thunk() {
return f(3, 4);
}
thunk(); // 返回7
对于存在回调的函数,可以采用增加一个回调函数作为参数
function f(x, y, cb){
setTimeout(_=>{
cb(x, y);
},1000)
}
function thunk(cb) {
return f(3, 4, cb);
}
thunk((x, y)=>{
console.log(x+y); // 7;
});
利用这种写法,我们可以对很多异步方法进行重新封装,可以让我们的焦点放到回调函数中,可以一定程度将参数输入和输出的处理过程进行分开开发。
4.2 生成器委托
在生成器中,可以嵌套iterable
对象,嵌套iterable
对象的时候并使用yield*
,当执行到yield* iterable
的时候,会将运行流程交给iterable
对象的iterator
。并在继续执行时调用其next
方法。
function *g1() {
yield 2;
yield 3;
}
function *g2() {
yield 1;
yield* g1(); // 或者yield *g1();
yield 4;
}
var it = g2();
console.log(it.next()); // {value: 1, done: false};
console.log(it.next()); // {value: 2, done: false};
console.log(it.next()); // {value: 3, done: false};
console.log(it.next()); // {value: 4, done: false};
console.log(it.next()); // {value: undefined, done: true};
5 总结
生成器是一个很有意思的东西,使用生成器可以将异步操作进一步升级,用一种同步编码风格去处理异步的情况。
6 参考
《你不知道的Javascript(中卷)》
MDN Generator