ES6 Generator函数
1. 简介
基本概念
Generator 函数是 ES6 提供的一种异步编程解决方案。Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数是一个普通函数,但是有两个特征:
- 一是,function关键字与函数名之间有一个星号
*
, - 二是,函数体内部使用
yield
表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
console.log(hw,'hw');
for (const item of hw) {
console.log(item,'item');
}
console.log(hw.next()); // { value: 'hello', done: false }
console.log(hw.next()); // { value: 'world', done: false }
console.log(hw.next()); // { value: 'ending', done: true }
console.log(hw.next()); // { value: undefined, done: true }
yield表达式
由于 Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。 - 如果该函数没有
return
语句,则返回的对象的value属性值为undefined。
与Interator接口的关系
function* g() {
yield 1;
yield 2;
yield 3;
};
var myIterable = {};
myIterable[Symbol.iterator] = g;
console.log([...myIterable]); // [1, 2, 3]
let gen = g();
console.log(gen[Symbol.iterator]() === gen); // true
Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被…运算符遍历了。
g是一个 Generator 函数,调用它会生成一个遍历器对象gen。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己
2. next方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
console.log(reset,'reset');
if(reset) { i = -1; }
}
}
var g = f();
console.log(g.next()); // { value: 0, done: false }
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
console.log(g.next(true)); // { value: 0, done: false }
如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
3. for…of 循环
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
// for...of 循环
for (let v of foo()) {
console.log(v);
}
// 1
// 2
// 3
// 4
// 5
// 扩展运算符
console.log([...foo()]); // [1, 2, 3, 4, 5]
// Array.from 方法
console.log(Array.from(foo())); // [1, 2, 3, 4, 5]
// 解构赋值
let [x, ...y] = foo();
console.log(x); // 1
console.log(y); // [2, 3, 4, 5]
4. Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。
throw方法被内部捕获以后,会附带执行到下一条yield表达式,这种情况下等同于执行一次next方法。
var gen = function* gen(){
try {
yield 1;
} catch (e) {
yield 2;
}
yield 3;
}
var g = gen();
g.next() // { value:1, done:false }
g.throw() // { value:2, done:false }
g.next() // { value:3, done:false }
g.next() // { value:undefined, done:true }
上面代码中,g.throw方法被内部捕获以后,等同于执行了一次next方法,所以返回{ value:2, done:false }。另外,也可以看到,只要 Generator 函数内部部署了try…catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
5. Generator.prototype.return()
Generator 函数返回的遍历器对象,还有一个return()
方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果 Generator 函数内部有·try…finally·代码块,且正在执行try代码块,那么return()方法会导致立刻进入·finally·代码块,执行完以后,整个函数才会结束。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
6. next()、throw()、return() 的共同点
它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式
next()
是将yield表达式替换成一个值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;
throw()
是将yield表达式替换成一个throw语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
return()
是将yield表达式替换成一个return语句
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
7. yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for…of循环。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
let i = concat([1,2],[3]);
console.log(i.next()); // {value: 1, done: false}
console.log(i.next()); // {value: 2, done: false}
console.log(i.next()); // {value: 3, done: false}
console.log(i.next()); // {value: undefined, done: true}
如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据
function* foo() {
yield 2;
yield 3;
return "foo";
}
function* bar() {
yield 1;
var v = yield* foo();
console.log("v: " + v);
yield 4;
}
var it = bar();
it.next()
//{value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
8. 作为对象属性的 Generator 函数
let obj = {
* myGeneratorMethod() {
···
}
};
相当于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
9. Generator 函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
Generator 函数也不能跟new命令一起用,会报错。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
obj // {a: 1, b: 2}
f.next(); // Object {value: 3, done: false}
obj // {a: 1, b: 2, c: 3}
f.next(); // Object {value: undefined, done: true}
obj // {a: 1, b: 2, c: 3}
或
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
console.log(f.next()); // Object {value: 2, done: false}
console.log(f.a, f.b, f.c); // 1 2 undefined
console.log(f.next()); // Object {value: 3, done: false}
console.log(f.a, f.b, f.c); // 1 2 3
console.log(f.next()); // Object {value: undefined, done: true}
console.log(f.a, f.b, f.c); // 1 2 3