Generator 函数(1)
1.Generator函数的概述
Generator函数是ES6提供的一种一部编程解决方案,语法行为与传统函数完全不同。对于Generator函数有多种理解角度。从语法上看,首先可以将它理解为一个状态机,封装了多个内部状态。执行Generator函数返回一个遍历器对象。
形式上,Generator函数是一个普通函数,但是有两个特征:一是function命令与函数名之间有一个星号;二十函数体内部使用yield语句定义不同的内部状态。(yield => 产出)
1.1第一次使用Generator函数
{
function* helloWolrdGenerator(){
yield 'hello';
yield 'world';
return 'ending';
}
let hw = helloWolrdGenerator();
6(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}
}
解析:上面代码定义了一个Generator函数,他的内部有两个yield语句,即该函数有三个状态:hello,world,return语句(结束执行)
调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是iterator对象。必须调用遍历器的next方法,使指针移动到下一个状态。换言之,Generator是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
next 返回的value属性和done,value为值,done为判断遍历是否结束。
1.2yield表达式
由于Generator函数返回的遍历器对象只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。
yield语句就是暂停标志。
遍历器对象的next方法的运行逻辑如下:
1.遇到yield语句就暂停执行后面的操作,并将紧跟在后面的yield后的表达式的值作为返回的对象的value属性值。
2.下一次调用next方法再继续往下执行,知道遇到吓一跳yield语句。
3.如果没有再遇到新的yield语句,就一直运行到函数结束,知道return语句为止。
4.如果该函数没有return语句,则返回对象的value属性值undefined。
{
//yield表达式时惰性求值的
function* add(){
yield 123 + 456;
}
// 上述的yield表达式不会立即求值,只有当next方法将指针移到这个位置才求值。
}
{
//yield表达式不能再其他函数内部使用
function sum(a,b){
yield(a+b);
}
sum(1,2);
//yield is not defined
(function(){
yield 1;
})()
//Unexpected number
}
1.3与iterator接口的关系
由于Generator函数就是遍历器生成函数,因此可以将Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有iterator接口。
{
let myIterator = {};
myIterator[Symbol.iterator] = function* (){
yield 1;
yield 2;
return 3;
};
console.log([...myIterator]);
//[1,2]
}
// 上面的代码中,Generator函数赋值给Symbol.iterator属性,从而使得myIterator对象具有了iterator接口,
// 可以被扩展运算符遍历。
//
{
function* gen(){
}
let g = gen();
console.log(g[Symbol.iterator]() === g);
// /true
}
//上面的代码中,gen是一个Generator函数,调用会生成一个遍历器对象g。他的Symbol.iterator
// 属性也是一个遍历器对象生成函数,执行后返回他自己。
2.next方法的参数
yield语句本身没有返回值,或者说是返回undefined。next方法可以带有一个参数,该参数会被当作上一条yield语句的返回值。
{
function* f(){
for(let i = 0 ; true; i ++){
let reset = yield i;
if(reset){
i = -1;
}
console.log(reset);
}
}
let g = f();
console.log(g.next()); //0
console.log(g.next()); //1
console.log(g.next(true)); //0
console.log(g.next());//1
console.log(g.next());//2
}
上面的代码先定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行带yield语句时,变量reset的值
总是undefined。当next方法带有一个参数true时,当前的变量reset就被重置为这个参数true,这时i=-1,下一圈开始从-1递增。
只修改一次状态
这个功能具有很重要的语法意义。Generator函数从暂停状态到恢复状态,上下文都是不变的。通过next方法的参数就有办法在Generator
函数开始运行后继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段从外部向内部注入不同的值。
{
//不传入参数
function* foo(x){
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return (x + y + z);
}
let a = foo(5);
a.next();
// {value:6,done:false}第一次返回x+1 为6
a.next();
// {value:NaN,done:false}
// 因为这时y的值为 2 * undefined 为 NaN 所以NaN/3 = NaN
a.next();
//{value:NaN,done:true}
//因为return的为 5 + NaN +undefined 所以为NaN
}
{
//传入参数
function* foo(x){
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return (x + y + z);
}
let a = foo(5);
console.log(a.next());
// {value:6,done:false}第一次返回x+1 为6
console.log(a.next(6));
// {value:4,done:false}
// 因为这时y的值为 12 所以value = 4
console.log(a.next(8));
//{value:33,done:true}
//因为z=8 y = 12 x = 5 所以为25
}
因为next方法的参数表示的上一条yield语句的返回值,所以第一次使用next方法,V8引擎默认无效
可以自己封装函数来完成第一次传递参数
参考<ES6标准入门> p324
3.for…of循环
for...of循环可以自动遍历Generator函数生成的Iterator对象,且此时不再需要调用next()方法。
for...of 循环
{
function* foo(){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for(let k of foo()){
console.log(k);
//1 2 3 4 5
}
}
一旦next方法返回对象的done属性为true,for...of循环就会停止,不包括return对象。
{
//Generator实现斐波那契
function* fibonacci(){
let [prev, curr] = [0,1];
for(;;){
[prev,curr] = [curr, prev + curr ];
yield curr;
}
}
for(let n of fibonacci()){
if(n > 10) break;
console.log(n)
//1 2 3 5 8
}
}
原生对象没有遍历接口,通过generator函数为他加上这个接口就可以使用for...of循环了
{
function* objcetEntires(obj){
let propKeys = Reflect.ownKeys(obj);
//Reflect.ownKeys 用于返回对象的所有属性
for(let propKey of propKeys){
yield [propKey,obj[propKey]]
}
}
let jane = {
first:'jane',
second:'Doe'
}
for(let [key,value] of objcetEntires(jane)){
console.log(key + ':' + value);
//first:jane
//second:Doe
}
}
{
// 第二种写法
function* objcetEntires(){
let propKeys = Object.keys(this);
for(let propKey of propKeys){
yield [propKey,this[propKey]];
}
}
let jane = {
first:'jane',
second:'Doe'
}
jane[Symbol.iterator] = objcetEntires;
for(let [key,value] of jane){
console.log(key + ':' + value);
//first:jane
//second:Doe
}
}
除了for...of循环,扩展运算符(...),解构赋值和Array.from方法内部调用的都是遍历器接口,这意味着
他们都可以将Generator函数返回的Iterator对象作为参数。
4.Generator.prototype.throw()
Generator函数返回的遍历器对象都有一个throw方法,可以在函数体外抛出错误,然后再Generator函数体内捕获
{
let g = function* (){
try{
yield;
} catch(e) {
console.log('内部捕获',e);
}
}
let i = g();
console.log(i.next());
try{
i.throw('a');
i.throw('b');
} catch(e) {
console.log('外部捕获',e);
}
//内部捕获 a
//外部捕获 b
}
上面的代码中,遍历器对象i连续抛出两个错误,第一个错误被Generator 函数体内的catch语句捕获。i第二次抛出错误,
由于Generator函数内部的catch语句已经执行过了,不会在捕捉到这个错误了,所以这个错误被抛出了Generator函数体,被函数体外的catch语句捕获。
{
let g = function* (){
try{
yield;
} catch(e) {
console.log('内部捕获',e);
}
}
let i = g();
console.log(i.next());
try{
i.throw('a');
i.throw('b');
} catch(e) {
console.log('外部捕获',e);
}
// 外部捕获 a
}
如果不执行Generator函数,则外部会捕获a这个错误
{
// 如果Generator函数内部没有部署try...catch代码块,那么throw方法抛出的错误将被外部try...catch代码块捕获。
let g = function* (){
while (true) {
yield;
console.log('内部捕获',e);
}
};
let i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch(e) {
console.log('外部捕获',e);
}
//外部捕获 a
}
如果generator函数内部部署了try…catch语句,那么遍历器的throw方法抛出的错误不影响下一次遍历;
否则导致程序报错;中断执行;throw方法被捕获以后会附带执行下一条yield命令;
{
let g = function* (){
try{
yield ('a')
} catch(e){
yield (e);
}
yield ('c');
yield ('d');
}
let i = g();
console.log(i.next());
//{value: "a", done: false}
console.log(i.throw('b'));
//{value: "b", done: false}
console.log(i.next());
//{value: "c", done: false}
console.log(i.next());
// {value: "d", done: false}
}
一旦Generator函数运行中出现错误,就不会继续执行下去了,js引擎认为这个Generator已经运行结束。
{
let g = function* (){
yield ('a');
yield ('b');
throw new Error('Generator an exception');
yield ('c');
}
let i = g();
//第一次运行
try{
console.log(i.next());
} catch(err){
console.log(err);
}
//{value: "a", done: false}
//第二次运行
try{
console.log(i.next());
} catch(err){
console.log(err);
}
//{value: "b", done: false}
//第三次运行
try{
console.log(i.next());
} catch(err){
console.log(err);
}
// Generator an exception
//第四次运行
try{
console.log(i.next());
} catch(err){
console.log(err);
}
//{value: "undefined", done: true}
}
当Generator函数执行过程中抛出错误,就不会继续执行下去了。如果此后调用next方法,将返回一个value属性为undefined
done属性为true的对象,js引擎默认认为这个Generator已经运行结束。
5.Generator.prototype.return()
Generator函数返回的遍历器对象还有一个return方法,可以返回给定的值,并中介Generator函数的遍历。
{
function* gen(){
yield 1;
yield 2;
yield 3;
return 4;
yield 5;
}
let g = gen();
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());
// {value: 4, done: true}
console.log(g.next());
// {value: undefined, done: true}
}
6.Generator函数this
Generator函数总是返回一个遍历器,ES6规定这个遍历器时Generator函数的实例,他也继承了Generator函数的prototype对象上的方法。
{
function* g(){};
g.prototype.name = function(){
return 'kjh'
}
let obj = g();
console.log(obj instanceof g);
//true
console.log(obj.name());
//kjh
}
上面代码表明,Generator函数g返回的遍历器obj时g的实例并且继承了原型上的方法。
{
function* g(){
this.a = 11;
}
let obj = g();
console.log(obj.a);
//undefined
}
如果将g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
也不能调用new因为他不是构造函数。
返回一个正常的实例,既可以调用next方法,又可以获得正常的this
{
function* f(){
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
let obj = {};
let f1 = f.call(obj);
console.log(f1.next());
//{value:2,done:false}
console.log(f1.next());
//{value:3,done:false}
console.log(f1.next());
console.log(obj.a); //1
}
7.Generator 与 状态机
Generator时实现状态机的最佳结构。
{
let ticking = true;
let clock = function(){
if(ticking){
console.log('Tick!');
}else{
console.log('tock!');
}
ticking = !ticking;
}
clock();
clock();
}
{
//使用Generator函数实现没运行一次状态修改一次
let clock = function* (){
while(true){
console.log('tick');
yield;
console.log('tock');
yield;
}
}
}
参考文献:<es6标准入门> 阮一峰著