Generator 函数有多种理解角度。语法上,首先能够把它理解成,Generator 函数是一个状态机,封装了多个内部状态。数组
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,仍是一个遍历器对象生成函数。返回的遍历器对象,能够依次遍历 Generator 函数内部的每个状态。数据结构
Generator 函数有两个特征:函数
function
关键字与函数名之间有一个星号
yield
表达式,定义不一样的内部状态(yield
在英语里的意思就是“产出”)function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false } value属性就是当前yield表达式的值,done属性为false,表示遍历尚未结束。
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true } done属性为true,表示遍历已经结束。
复制代码
上面代码定义了一个 Generator 函数helloWorldGenerator
,它内部有两个yield
表达式(hello
和world
),即该函数有三个状态:hello,world 和 return 语句(结束执行)。
调用方法与普通函数同样,可是调用,函数并不执行,返回一个指向内部状态的指针对象,也就是遍历器对象。 必须调用遍历器对象的next
方法,使得指针移向下一个状态。内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。
换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法能够恢复执行。this
因为 Generator 函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,因此其实提供了一种能够暂停执行的函数。yield
表达式就是暂停标志。所以等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
yield
表达式只能用在 Generator 函数里面,用在其余地方都会报错。lua
yield
表达式自己没有返回值,或者说老是返回undefined
。next
方法能够带一个参数,该参数就会被看成上一个yield
表达式的返回值。spa
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}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
复制代码
上面代码中,第二次运行next
方法的时候不带参数,致使 y
的值等于2 * undefined
(即NaN
),除以 3
之后仍是NaN
,所以返回对象的value
属性也等于NaN
。第三次运行Next
方法的时候不带参数,因此z
等于undefined
,返回对象的value
属性等于5 + NaN + undefined
,即NaN
。prototype
若是向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
方法用来启动遍历器对象,因此不用带有参数。code
next
方法的参数,也能够向Generator 函数内部输入值对象
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
复制代码
for...of
循环能够自动遍历 Generator 函数时生成的Iterator
对象,且此时再也不须要调用next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6; //return语句返回的,不包括在for...of循环之中
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
复制代码
除了for...of
循环之外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们均可以将 Generator 函数返回的 Iterator 对象,做为参数。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
复制代码
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
复制代码
一旦执行了catch
,捕捉了错误,Generator 函数就已经结束了,再也不执行下去了。
Generator 函数返回的遍历器对象,还有一个return
方法,能够返回给定的值,而且终结遍历 Generator 函数。
本质上是同一件事,能够放在一块儿理解。它们的做用都是让 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;
复制代码
若是在 Generator 函数内部,调用另外一个 Generator 函数,默认状况下是没有效果的。yield*
表达式,用来在一个 Generator 函数里面执行另外一个 Generator 函数。
function* foo() {
yield 'a';
yield 'b';
}
//普通方法调用foo() ==========================
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
//上面foo()的调用是没有效果的
//yield*表达式调用 =================================
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
表达式后面跟的是一个遍历器对象,须要在yield表达式后面加上星号,代表它返回的是一个遍历器对象。这被称为yield*
表达式。
yield*
后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of
循环。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
复制代码
上面代码说明,yield*
后面的 Generator 函数(没有return
语句时),不过是for...of
的一种简写形式,彻底能够用后者替代前者。反之,在有return
语句时,则须要用var value = yield* iterator
的形式获取return
语句的值。
yield*
命令能够很方便地取出嵌套数组的全部成员。
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
复制代码
Generator 函数g返回的遍历器obj
,是g的实例,并且继承了g.prototype
。可是,若是把g看成普通的构造函数,并不会生效,由于g返回的老是遍历器对象,而不是this
对象,也不能跟new命令一块儿用,会报错。
下面是一个变通方法。首先,生成一个空对象,使用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}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
复制代码
还有一个办法就是将obj换成F.prototype
。
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
复制代码
再将F改为构造函数,就能够对它执行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
复制代码