1,概念
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用yield
语句,定义不同的内部状态(yield
在英语里的意思就是“产出”)
function* create(){
yield '1';
yield '2';
return 'over';
}
var n = create();
//Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号
//不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
//必须调用遍历器对象的next方法,使得指针移向下一个状态
//也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止
//Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行
console.log(n.next());//Object {value: "1", done: false}
console.log(n.next());//Object {value: "2", done: false}
console.log(n.next());//Object {value: "over", done: true}
console.log(n.next());//Object {value: undefined, done: true}
//第三次调用,Generator函数从上次yield语句停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。
//next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),
//done属性的值true,表示遍历已经结束。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
ES6没有规定,
function
关键字与函数名之间的星号,写在哪个位置。这导致写法都能通过
由于Generator函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function
关键字后面。本书也采用这种写法。
2,yield
由于Generator函数返回的遍历器对象,只有调用
next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
语句就是暂停标志。
需要注意的是,
yield
语句后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
Generator函数可以不用
yield
语句,这时就变成了一个单纯的暂缓执行函数。function* foo(){
console.log('走起!');
}
var g = foo();
setTimeout(function(){
g.next();
},3000);
//上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。
//但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
//另外需要注意,yield语句只能用在 Generator 函数里面,用在其他地方都会报错。
var arr = [1,[[2,3],4],[5,6]];
var flg = function* (a){
var len = a.length;
for(var i=0;i<len;i++){
var item = a[i];
if(typeof item !== 'number'){
yield* flg(item);
}else{
yield item;
}
}
};
for(let f of flg(arr)){
console.log(f);//1 2 3 4 5 6
}
//另外,yield语句如果用在一个表达式之中,必须放在圆括号里面。
function* foo(){
console.log('hello'+yield);//SyntaxError
console.log('hello'+ yield 123);//SyntaxError
console.log('hello'+(yield));//正确
console.log('hello'+(yield 1));//正确
}
//yield语句用作函数参数或放在赋值表达式的右边,可以不加括号
function* f() {
f(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
3,与Iterator接口的关系
任意一个对象的
Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的
Symbol.iterator
属性,从而使得该对象具有Iterator接口。
var iterable = {};
iterable[Symbol.iterator] = function* (){
yield 1;
yield 2;
yield 3;
};
console.log([...iterable]);//[1, 2, 3]
//上面代码中,Generator函数赋值给Symbol.iterator属性,
//从而使得myIterable对象具有了Iterator接口,可以被...运算符遍历了。
//Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身
function* f(){
// to do
}
var g = f();
console.log(g[Symbol.iterator]() === g);//true
//上面代码中,f是一个Generator函数,调用它会生成一个遍历器对象g。
//它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己
4,for...of循环
for...of
循环可以自动遍历Generator函数时生成的Iterator
对象,且此时不再需要调用next
方法function* f(){
yield 1;
yield 2;
yield 3;
yield 4;
return 5;
}
for(let i of f()){
console.log(i);
}
//1 2 3 4
//上面代码使用for...of循环,依次显示4个yield语句的值。
//这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,
//所以上面代码的return语句返回的5,不包括在for...of循环之中
//下面是一个利用Generator函数和for...of循环,实现斐波那契数列的例子。
function* fibonacci(){
let [prev,curr] = [0,1];
for(;;){
[prev,curr] = [curr,prev+curr];
yield curr;
}
}
for(let n of fibonacci()){
if(n > 1000){
break;
}
console.log(n);
}
//从上面代码可见,使用for...of语句时不需要使用next方法。
利用
for...of
循环,可以写出遍历任意对象(object)的方法。原生的JavaScript对象没有遍历接口,无法使用
for...of
循环,通过Generator函数为它加上这个接口,就可以用了。
function* objectEntries(obj){
let propKeys = Reflect.ownKeys(obj);
for(let propKey of propKeys){
yield [propKey,obj[propKey]];
}
}
let arr = {
first:'andy',
second:'jack'
};
for(let [key,value] of objectEntries(arr)){
console.log(`${key}:${value}`);
}
//first:andy
//second:jack
//上面代码中,对象arr原生不具备Iterator接口,无法用for...of遍历。
//这时,我们通过Generator函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。
//加上遍历器接口的另一种写法是,
//将Generator函数加到对象的Symbol.iterator属性上面。
function* objectEntries1(obj){
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let arr1 = {
first:'andy',
second:'jack'
};
arr1[Symbol.iterator] = objectEntries1;
for(let [key,value] of arr1){
console.log(`${key}:${value}`);
}
//first:andy
//second:jack
//除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。
//这意味着,它们都可以将Generator函数返回的Iterator对象,作为参数。
function* numbers(){
yield 1;
yield 2;
return 3;
yield 4;
}
//扩展运算符
console.log([...numbers()]);//[1, 2]
//Array.from()
console.log(Array.from(numbers()));//[1, 2]
//解构赋值
let [x,y] = numbers();
console.log(x,y);//1 2
//for...of
for(let n of numbers()){
console.log(n);
}
//1
//2
5,Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个
throw
方法,可以在函数体外抛出错误,然后在Generator函数体内捕获。
var f = function* (){
try{
yield ;
}catch (e){
console.log('inner '+e);
}
};
var i = f();
i.next();
try{
i.throw('a');
i.throw('b');
}catch (e){
console.log('outer '+e);
}
//inner a
//inner b
//上面代码中,遍历器对象i连续抛出两个错误。
//第一个错误被Generator函数体内的catch语句捕获。i第二次抛出错误,
//由于Generator函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,
//所以这个错误就被抛出了Generator函数体,被函数体外的catch语句捕获。
//throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。
var l = function* (){
try {
yield ;
}catch (e){
console.log(e);
}
};
var n = l();
n.next();
n.throw(new Error('wrong!'));
//Error: wrong!
//注意,不要混淆遍历器对象的throw方法和全局的throw命令。
//上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。
//后者只能被函数体外的catch语句捕获。
var m = function* (){
while (true){
try{
yield ;
}catch (e){
if(e!='a'){
throw e;
}
console.log('inner '+e);
}
}
};
var n1 = m();
n1.next();
try{
throw new Error('a');
throw new Error('b');
}catch (e){
console.log('outer '+e);
}
//outer Error: a
//上面代码之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,
//就不会再继续try代码块里面剩余的语句了。
//如果Generator函数内部没有部署try...catch代码块,
//那么throw方法抛出的错误,将被外部try...catch代码块捕获
var f = function* (){
while (true){
yield ;
console.log('inner '+e);
}
};
var i = f();
i.next();
try{
i.throw('a');
i.throw('b');
}catch (e){
console.log('outer '+e);
}
//outer a
//上面代码中,Generator函数g内部没有部署try...catch代码块,
//所以抛出的错误直接被外部catch代码块捕获
//如果Generator函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行
var g = function* g(){
yield console.log('h');
yield console.log('w');
};
var i1 = g();
i1.next();
i1.throw();
//h
//Uncaught undefined
//throw方法被捕获以后,会附带执行下一条yield语句。也就是说,会附带执行一次next方法。
var g1 = function* g1(){
try{
yield console.log('a');
}catch (e){
}
yield console.log('b');
yield console.log('c');
};
var n = g1();
console.log(n.next());
n.throw();
console.log(n.next());
//上面代码中,g.throw方法被捕获以后,自动执行了一次next方法,所以会打印b。
//另外,也可以看到,只要Generator函数内部部署了try...catch代码块,
//那么遍历器的throw方法抛出的错误,不影响下一次遍历。
6,Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个
return
方法,可以返回给定的值,并且终结遍历Generator函数。
function* g(){
yield 1;
yield 2;
yield 3;
}
var f = g();
console.log(f.next());//Object {value: 1, done: false}
console.log(f.return('foo'));//Object {value: "foo", done: true}
console.log(f.next());//Object {value: undefined, done: true}
//上面代码中,遍历器对象g调用return方法后,返回值的value属性就是return方法的参数foo。
//并且,Generator函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true。
//如果return方法调用时,不提供参数,则返回值的value属性为undefined
function* g1(){
yield 1;
yield 2;
yield 3;
}
var f1 = g1();
console.log(f1.next());//Object {value: 1, done: false}
console.log(f1.return());//Object {value: undefined, done: true}
//如果Generator函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。
function* num(){
yield 1;
try{
yield 2;
yield 3;
}finally {
yield 4;
yield 5;
}
yield 6;
}
var f2 = num();
console.log(f2.next());//Object {value: 1, done: false}
console.log(f2.next());//Object {value: 2, done: false}
console.log(f2.return(7));//Object {value: 4, done: false}
console.log(f2.next());//Object {value: 5, done: false}
console.log(f2.next());//Object {value: 7, done: true}
//上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。
7,yiled*语句
如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。
function* foo(){
yield 'a';
yield 'b';
}
function* bar(){
yield 'x';
foo();
yield 'y';
}
for(let i of bar()){
console.log(i);
}
//x
//y
//上面代码中,foo和bar都是 Generator 函数,在bar里面调用foo,是不会有效果的。
这个就需要用到
yield*
语句,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* foo(){
yield 'a';
yield 'b';
}
function* bar(){
yield 'x';
yield* foo() ;
yield 'y';
}
for(let i of bar()){
console.log(i);
}
//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
的一种简写形式,完全可以用后者替代前者。反之,则需要用
var value = yield* iterator
的形式获取
return
语句的值。
如果
yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
function* g(){
yield* ['a','b','c'];
}
console.log(g().next());//Object {value: "a", done: false}
//上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
//实际上,任何数据结构只要有Iterator接口,就可以被yield*遍历。
let arr = (function*(){
yield 'hello';
yield* 'hello';
})();
console.log(arr.next().value);//hello
console.log(arr.next().value);//h
console.log(arr.next().value);//e
//上面代码中,yield语句返回整个字符串,yield*语句返回单个字符。
//因为字符串具有Iterator接口,所以被yield*遍历。
function* foo(){
yield 2;
yield 3;
return 'foo';
}
function* bar(){
yield 1;
var v = yield* foo();
console.log('v '+v);
yield 4;
}
var f = bar();
console.log(f.next());//Object {value: 1, done: false}
console.log(f.next());//Object {value: 2, done: false}
console.log(f.next());//Object {value: 3, done: false}
console.log(f.next());//'v foo' Object {value: 4, done: false}
console.log(f.next());//Object {value: undefined, done: true}
//上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数foo的return语句,向函数bar提供了返回值
function* genF(){
yield 'a';
yield 'b';
return 'over';
}
function* logF(obj){
let result = yield* obj;
console.log(result);
}
[...logF(genF())];//over
yield*
命令可以很方便地取出嵌套数组的所有成员。
function* num(n){
if(Array.isArray(n)){
for(let i=0;i<n.length;i++){
yield* num(n[i]);
}
}else{
yield n;
}
}
const n = ['a',['b','c'],['d']];
for(let x of num(n)){
console.log(x);
}
//a
//b
//c
//d
8,作为对象的属性的Generator函数
//如果一个对象的属性是Generator函数,可以简写成下面的形式。
let obj = {
*init(){
//to do
}
};
//init属性前面有一个星号,表示这个属性是一个Generator函数
//它的完整形式如下,与上面的写法是等价的
let obj = {
init:function*(){
//to do
}
}
9,Gnerator函数中的this
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的
prototype
对象上的方法
function* f(){
}
f.prototype.sayHello = function(){
return 'hi';
};
let obj = f();
console.log(obj instanceof f);// true
console.log(obj.sayHello());// hi
//Generator函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。
//但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象
function* f1(){
this.a = 10;
}
let obj1 = f1();
console.log(obj1.a);//undefined
//上面代码中,Generator函数f1在this对象上面添加了一个属性a,但是obj1对象拿不到这个属性
//Generator函数也不能跟new命令一起用,会报错
function* F(){
yield this.x = 2;
yield this.y = 3;
}
new F();//Uncaught TypeError: F is not a constructor
//new命令跟构造函数F一起使用,结果报错,因为F不是构造函数
如何解决呢?
下面是一个变通方法。首先,生成一个空对象,使用call
方法绑定Generator函数内部的this
。这样,构造函数调用以后,这个空对象就是Generator函数的实例对象了
function* F(){
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
console.log(f.next());//Object {value: 2, done: false}
console.log(f.next());//Object {value: 3, done: false}
console.log(f.next());//Object {value: undefined, done: true}
console.log(obj.a);//1
console.log(obj.b);//2
console.log(obj.c);//3
//上面代码中,首先是F内部的this对象绑定obj对象,然后调用它,返回一个Iterator对象。
//这个对象执行三次next方法(因为F内部有两个yield语句),完成F内部所有代码的运行。
//这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。
上面代码中,执行的是遍历器对象
f
,但是生成的对象实例是
obj
,有没有办法将这两个对象统一呢?
一个办法就是将obj
换成F.prototype
。
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.next());//Object {value: 3, done: false}
console.log(f.next());//Object {value: undefined, done: true}
console.log(f.a);//1
console.log(f.b);//2
console.log(f.c);//3
//再将F改成构造函数,就可以对它执行new命令了。
function* bar(){
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function Foo(){
return bar.call(bar.prototype);
}
var foo = new Foo();
console.log(foo.next());
console.log(foo.next());
console.log(foo.next());
console.log(foo.a);//1
console.log(foo.b);//2
console.log(foo.c);//3