参考:ECMAScript 6 入门
Generator 函数概览
调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针(类比Iterator)。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值(yield本来就是产出的意思,它代表后面的计算值就是它的产出值);done属性是一个布尔值,表示是否遍历结束。
举例:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator(); // 生成一个生成器对象(好比生成一个遍历器对象)
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
为什么 Generator 函数叫 Generator
因为 Generator 函数里用了yield啊,yield就是产出的意思,所以它叫生成器函数。
yield用法的注意事项(语法层面的东西我认为是非重点,如果开发过程中语法报错,查文档就可以了):
- yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
- yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
Generator 与 Iterator 的关系:
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
我的问题:既然 Generator 实现的就是遍历功能,只用 Iterator 就行了,为什么还多此一举再弄一个 Generator?
理解yield的返回值:
next方法返回的是yield关键字后面表达式的计算值,但是yield和它后面的表达式组合起来的表达式没有返回值,或返回值是undefined,如何理解?
第一种情况举例:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator(); // 生成一个生成器对象(好比生成一个遍历器对象)
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
第二种情况举例:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
我们预想的结果是:y为12,z为4,return值为(5+12+4),但实际结果是:y为NaN,z为NaN,return值为(5+NaN+NaN),所以我们得出结论,在var y = 2 * (yield (x + 1));
这个语句中,(yield (x + 1))
这个表达式没有返回值。
那如何让类似(yield (x + 1))
的表达式有返回值呢?
答案是通过next方法值,请看例子:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
在上述例子中,b.next(12)
中的12定义了(yield (x + 1))
的返回值。也就是说,next方法里传的参数定义了上一个yield表达式的返回值(隐藏了一个信息就是,调用的第一个next方法不用传参数,即使传了也没用,因为它没有上一个yield表达式)。所以在上例中,x为5,y为2*12
,z为13
,return值为42。
含有yield表达式的语句是如何运行的?
我本来以为第一次执行next方法的结果是:Started, 1. undefined;同理,第二次执行next方法是:2. undefined
但是,就我们看到的结果,很明显执行next方法的时候,它不会执行它所在的语句,只会返回yield后面表达式的计算值,另外从上一个包含yield的语句开始执行。
for...of 循环:
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
两点:
- 为什么打印结果没有6?
一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
- 为什么for循环里这样写
let v of foo()
?
因为我们要从遍历器对象里遍历,foo()
返回一个遍历器对象;
那为什么平时我们遍历数组等时,没见调用方法?因为它默认隐式调用了。
无论解构还是什么,都是操作遍历器对象的。比如:
let array = [1,2,3];
console.log(...array); // array默认调用了生成遍历器对象的方法
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2],必须要numbers(),因为它才是遍历器对象
Generator.prototype.throw() 知识点太琐碎
内部捕获错误这种机制的好处:
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try...catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch语句就可以了。
我对上面这段话的理解是:
一般的做法,如果我们要对抛出的每个错误都处理,我们要这样做:
try {
throw new Error('error 1');
} catch (e) {
console.log('error 1:', e);
}
try {
throw new Error('error 2');
} catch (e) {
console.log('error 2:', e);
}
如果写成如下这样我们如下:
try {
throw new Error('error 1');
throw new Error('error 2');
} catch (e) {
console.log(e);
}
当捕获到error 1的错误后,就不再执行了。而且也没有机会执行throw new Error('error 2');
这句。
使用Generator.prototype.throw()
就可以完美的将两种优点结合起来:
try {
g.throw('error 1'); // 这个错误将会在 Generator 函数内部处理
g.throw('error 2'); // 这个错误将会交由下面的catch处理
} catch (e) {
console.log(e); // 打印 error 2
}
遍历器对象抛出的错误优先被 Generator 函数内定义的catch捕获处理,如果还有多余的错误处理不了了,遍历器对象将错误抛到外层。如果遍历器对象抛出的错误内外层都处理不了,那么程序将报错,直接中断执行。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a'); // 优先被内部的catch处理
i.throw('b'); // 内部的catch已经执行过一次,已无法处理错误,抛出到外层,交由外层处理。
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
}
var g = gen();
g.throw(1); //没执行过next方法,Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。
// Uncaught 1
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a;因为yield的暂停功能,执行完这个next方法后,程序还停留在try代码块中,所以下面的throw才可以被catch
g.throw() // b; 附带执行一次next方法
g.next() // c
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。(即使错误被外部捕获,next方法也不会再执行下去了)
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(); // 执行到这里时,会执行throw new Error('generator broke!')这句,抛出的这个错误又没被内部捕获,再执行next方法,Generator 函数就已经结束了
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
Generator.prototype.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 } 到这里就结束执行了,并返回了foo
g.next() // { value: undefined, done: true }
有一种情况,即使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 },因为还有finally要执行,所以运行不会结束,
// 它会当做执行一次next方法,并设置了运行结束后的返回值
g.next() // { value: 5, done: false }
g.next()
// { value: 7, done: true },finally代码块执行结束后,运行结束了,所以yield 6不会执行。
// 因为设置了最终返回值是7,所以执行这个next返回7
让我们来总结下都有哪些情况可以结束运行:
- 正常运行next到最后一个yield
- 遍历器对象抛出的错误没有被内部捕获
- 遍历器对象调用return方法,但finally会延迟结束情况的发生
next()、throw()、return() 的共同点
next()、throw()、return()这三个方法本质上是同一件事,它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
- next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。
- throw()是将yield表达式替换成一个throw语句。
- return()是将yield表达式替换成一个return语句。
如何在 Generator 函数中再调用 Generator 函数
关键词:yield*
function* foo() {
yield 'a';
yield 'b';
}
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"
带 return 的 Generator 函数
单独的:
Generator 函数中调用带 return 的 Generator 函数:
解释:如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数foo的return语句,向函数bar提供了返回值。
使用 Generator 函数进行递归
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]); //在些递归
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
Generator 函数作为对象属性如何定义?
一般的:
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
简写的:
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函数的this(要重点注意)
- Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。而Generator 函数里面的this并不指的是遍历器对象。
function* g() {
this.a = 11;
}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj.next();
obj instanceof g // true
obj.hello() // 'hi!'
obj.a // undefined;说明里面的this指向的不是遍历器对象
- 如何使用 Generator 函数里面的this?
答:Generator 函数本质上是加了语法糖的函数,是函数就可以通过改变执行上下文的方法改变里面的this指向(call或apply)
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj); // 使里面的this指向obj
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
- 如果我想在遍历器对象上访问属性呢?基于上一个的方法,再加上遍历器对象可以访问它的原型对象,那我们就把它的原型对象传进去。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
- 如果我想使用new呢?那就再上一步的基础上再包一层
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
// 用该方法再包一层
function F() {
return gen.call(gen.prototype);
}
var f = new F();
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3