ES6 Generator函数 语法 深入理解

前提

ES6中的Generator函数,这个函数与两个内部原生类有关,分别是GeneratorFunction类Generator类,这个两个类是内部原生类我们只能在谷歌浏览器的控制台上能看见,但是我们无法直接使用这两个类。

GeneratorFunction类

使用function关键字定义的普通函数,其本质是一个Function类的实例对象:

// 使用 function 关键字定义了一个普通函数
function normalFunction(){
	var variable = 1;
	console.log(variable);
}
// normalFunction 的原型对象就是 Function类 的 prototype 属性的值
Object.getPrototypeOf(normalFunction) === Function.prototype // true

使用 function关键字 和 *符号 定义的Generator函数,其本质是一个GeneratorFunction类的实例对象,GeneratorFunction类在代码中无法使用,暂时只能通过浏览器的控制台显示而知道它的存在:

function* generatorFunction(){
	yield 1;
	return 2;
}
// 获取 Generator函数 的原型对象
// GeneratorFunction {prototype: Generator, constructor: ƒ, Symbol(Symbol.toStringTag): "GeneratorFunction"}
var prototype = Object.getPrototypeOf(generatorFunction);
// 获取 Generator函数 的 构造函数对象
// ƒ GeneratorFunction() { [native code] }
var constructor = Object.getPrototypeOf(generatorFunction).constructor; 

// Generator函数 的构造函数对象上的prototype属性 === Generator函数 的原型对象
constructor.prototype === prototype; // true

GeneratorFunction类Function类的子类,所以Generator函数本质依然是一个函数对象:

// GeneratorFunction类 的原型对象的原型对象是 Function类 的原型对象
Object.getPrototypeOf(Object.getPrototypeOf(generatorFunction)) === Function.prototype; // true
// GeneratorFunction类 的构造函数对象的原型对象是 Function类 的构造函数对象
Object.getPrototypeOf(Object.getPrototypeOf(generatorFunction).constructor) === Function; // true

既然Generator函数本质上也是一个函数对象,那么Generator函数对象和普通函数对象在属性上其实大部分是一致的,但其中有一个属性是有很大的区别的:prototype属性。

  • 普通函数对象C在新建完成后,会自动以Object.prototype作为原型对象创建一个新对象N,然后在对象N上添加一个constructor属性,属性的值就是对象C,最后将对象N作为对象C的prototype属性的值。

    Reflect.ownKeys(normalFunction); // ["length", "name", ... "prototype"]
    
    // 普通函数 对象的原型对象上是没有 prototype 属性的,所以 普通函数 对象的上 prototype 属性不是来自继承
    Object.getPrototypeOf(normalFunction).prototype; // undefined
    
    Object.getPrototypeOf(normalFunction.prototype) === Object.prototype; // true
    normalFunction.prototype.constructor === normalFunction; // true
    
  • GeneratorFunction类原型对象上是存在prototype属性的,按照继承原型链的规则,那么Generator函数对象上的prototype属性应该是继承自原型对象的,但是实际上不是,Generator函数实例对象在新建完成后,会自动以GeneratorFunction类原型对象上的prototype属性的值作为原型对象创建一个新对象G,然后将对象G作为Generator函数对象的prototype属性的值,但是注意这里不会和普通函数一样给prototype属性的值的constructor属性进行重新赋值。

    Reflect.ownKeys(generatorFunction); // ["length", "name", "prototype"]
    
    // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}
    Object.getPrototypeOf(generatorFunction).prototype; 
    generatorFunction.prototype === Object.getPrototypeOf(generatorFunction).prototype; // false
    
    Object.getPrototypeOf(generatorFunction.prototype) === Object.getPrototypeOf(generatorFunction).prototype; // true
    

GeneratorFunction类的原型对象 是所有的Generator函数实例对象的原型对象,但是其不止这一个身份,其也是Generator类的构造函数对象,只是这个构造函数对象极其特殊。

总结:GeneratorFunction类Function类的子类,Generator函数GeneratorFunction类的实例对象,GeneratorFunction类是定义声明Generator函数的模版。

Generator类

JavaScript中我们想要定义一个类的时候,首先需要一个函数对象,其次这个函数对象上必须有一个prototype属性,而prototype属性的值是一个对象,这个对象上有一个constructor属性,而constructor属性的值指向前面的函数对象。那么我们可以以函数对象的函数名为类名,函数对象是这个类的构造函数,函数对象上的prototype属性的值则是类的原型对象。

按照上文的描述,我们可以把 GeneratorFunction类的原型对象 认为是一个类的构造函数对象。

// 获取 GeneratorFunction类 的原型对象
var generatorFunctionPrototype = Object.getPrototypeOf(function*(){});

// generatorFunctionPrototype 的 prototype 属性的值是一个对象,下面是谷歌浏览器控制台上的显示
// Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}
generatorFunctionPrototype.prototype;

// generatorFunctionPrototype 的 prototype 属性值的 constructor 属性的值等于 generatorFunctionPrototype
generatorFunctionPrototype.prototype.constructor === generatorFunctionPrototype; // true

// 而且 generatorFunctionPrototype 对象其实是与普通函数平级的,有着同一个原型对象
// 所以我们可以将 generatorFunctionPrototype 对象当作一个特殊类的特殊构造函数对象。 
Object.getPrototypeOf(generatorFunctionPrototype) === Object.getPrototypeOf(function(){}); // true

按照谷歌浏览器控制台上的显示我们将这个特殊内部类称为Generator类,如此上面代码中的generatorFunctionPrototype就是Generator类的构造函数对象。

Generator函数则可以认为是Generator类子类的构造函数对象

function* generatorFunction(){}
// Generator函数 的原型对象是 generatorFunctionPrototype
Object.getPrototypeOf(generatorFunction) === generatorFunctionPrototype;
// Generator函数 的 prototype 属性的值的原型对象是 generatorFunctionPrototype 的 prototype 属性的值
Object.getPrototypeOf(generatorFunction.prototype) === generatorFunctionPrototype.prototype;

我们此时完全可以将Generator函数的函数名作为一个类的类名,且这个类是Generator类的子类,但是这个类有一个缺陷,Generator函数prototype属性值的constructor属性的值并不是指向Generator函数,而是从原型对象上继承来的:

generatorFunction.prototype.constructor === generatorFunction; // false
generatorFunction.prototype.constructor === generatorConstructor // true

所以我们本文中将Generator函数作为Generator类的次级构造函数对象,GeneratorFunction类的原型对象作为Generator类的初始构造函数对象,Generator函数prototype属性值作为Generator类的次级原型对象,GeneratorFunction类的原型对象的prototype属性值作为Generator类的初始原型对象。

猜测:之所以有次级与初始的分别,估计是为了Generator类实例对象的归属进行区分,因为Generator类实例对象是以次级原型对象进行创建的,而次级原型对象又是以初始原型对象创建的,那么每定义一个Generator函数就会出现一个新的次级原型对象,同时每调用一次Generator函数就会返回一个属于次级原型对象的Generator类实例对象。如此初始原型对象只会有一个,而次级原型对象会有多个,如果想再所有Generator类实例对象上添加新属性,那直接在初始原型对象上添加就行,而如果只是想在某个Generator函数返回的Generator类实例对象上添加属性,则在次级原型对象上添加就行。

一、Generator函数 调用

函数调用大致可以分为两种,一种通过new关键字调用,另一种被某个对象调用。
普通函数两种调用方法都可以使用,而Generator函数只能使用被某个对象调用这个方法:

function normalFunction(){
	var variable = 1;
	console.log(variable);
}
function* generatorFunction(){
	yield 1;
	return 2;
}
// 被全局对象window调用
normalFunction(); // 运行正常
// 被{}对象调用
normalFunction.call({a:1});
new normalFunction(); // 运行正常
// 被全局对象window调用
generatorFunction();	// 运行正常
// 被{}对象调用
generatorFunction.call({b:2});
new generatorFunction(); // 报错

普通函数Generator函数在调用时的区别:

  • 普通函数在被某个对象调用时,是直接执行函数体中的代码。而通过new关键字调用则会先以函数实例对象的prototype属性的值作为原型对象创建一个对象N,然后在用对象N调用这个函数,对对象N进行初始化,此时我们可以认为函数名是一个类名,对象N是这个类的实例对象,最后将对象N作为函数的返回值。

  • Generator函数在被某个对象W调用时,则不在是直接执行Generator函数的函数体中的代码了,而是先以Generator函数函数实例对象的prototype属性的值作为原型对象创建一个对象G(这个对象就是Generator类实例对象),然后才会使用对象W(注意不是新建的Generator类实例对象去调用函数)调用Generator函数去执行函数体代码,但是这里的调用不是平常的函数调用,JS解释器从Generator函数实例对象的内存中取出字符串形式的函数体后,解析字符串形式的函数体,解析成功会产生一个属于Generator函数上下文执行环境,但是不会将这个新生上下文执行环境加入上下文执行环境栈(压栈)中从而执行函数体代码(此时产生了函数暂停执行的效果),而是将这个上下文执行环境挂靠在对象G上,然后将对象G作为Generator函数的返回值。

    function* He(){
    	console.log('He');
    }
    He(); 
    // He()调用的返回值
    // He { // 谷歌浏览器控制台上的显示
    //	[[GeneratorStatus]]: "suspended",
    // 	[[GeneratorFunction]]: ƒ* He(),
    // 	[[GeneratorReceiver]]: Window
    // }
    

Generator函数被某个对象调用后,返回的是一个Generator类实例对象,这个实例对象通过浏览器控制台的显示可以大致将其包含的信息分为三个部分:

  • [[GeneratorStatus]]:这个内部属性的值是Generator函数的执行状态,其有三个值,分别是suspended(属于函数的上下文执行环境不在上下文执行环境栈中,而是移出执行栈中并被挂靠冻结了,代表着函数体暂停执行)、running(属于函数的上下文执行环境重新加入到上下文执行环境栈并执行,代表着函数体正在执行中)、closed(函数执行结束,无法在使用Generator类实例对象去重新执行函数)
  • [[GeneratorFunction]]:这个内部属性的值指向了被调用的Generator函数
  • [[GeneratorReceiver]]:这个内部属性的值指向了调用Generator函数的某个对象
function* generator(){
	// 此处打了个断点,可以清楚知道g1这个 Generator类 实例对象上挂靠的 Generator函数 此时的状态
	console.log(g1); // {[[GeneratorStatus]]: "running", [[GeneratorFunction]]: ƒ* generator(), [[GeneratorReceiver]]: obj{a:1,b:2,c:3},}
	debugger;
	yield 1;
	console.log('Generator');
	yield 2;
}
var obj = {a:1,b:2,c:3};
var g1 = generator.call(obj);
// g1是一个 Generator类 的实例对象,这个对象上挂靠了一个处于暂停执行状态下的 Gnerator函数
g1; // {[[GeneratorStatus]]: "suspended", [[GeneratorFunction]]: ƒ* generator(), [[GeneratorReceiver]]: obj{a:1,b:2,c:3},}
// 当调用了 Generator类 的实例对象的next方法后,挂靠在对象上的处于暂停执行状态下的 Gnerator函数会重新执行
g1.next(); // {value: 1, done: false}
g1.next(); // {value: 2, done: false}
g1.next(); // {value: undefined, done: true}
// 当 Generator函数 中的函数体代码都执行完成后,在此查看挂靠在g1对象上的 Generator函数 的状态已经是执行结束了
g1; // {[[GeneratorStatus]]: "closed", [[GeneratorFunction]]: ƒ* generator(), [[GeneratorReceiver]]: obj{a:1,b:2,c:3},}

Generator类实例对象的原型链上的某个原型对象具有Symbol.iterator属性,所以我们可以将Generator类实例对象作为一个实现了Iteratble接口的对象,而实现了Iteratble接口的对象可以被for...of等语法进行循环遍历:

function* generator(){
	yield 1;
	yield 2;
	yield 3;
}
var g1 = generator();
for(let y of g1){
	console.log(y);
} // 输出打印:1、2、3

Generator类实例对象的原型链上的某个原型对象具有next属性,所以我们也可以将Generator类实例对象作为一个遍历器对象。其实Generator类实例对象的Symbol.iterator属性方法的返回值就指向对象本身。

function* generator(){
	yield 1;
	yield 2;
	yield 3;
}
var g1 = generator();
g1[Symbol.iterator]() === g1;

var nextResult = g1.next();
while(!nextResult.done){
	console.log(nextResult.value);
	nextResult = g1.next();
} // 输出打印:1、2、3

二、Generator类 实例对象 的使用

Generator函数返回的Generator类实例对象身上挂靠了一个冻结并移出执行栈的上下文执行环境,这个执行环境属于返回实例对象的Generator函数的函数体的。

Generator类实例对象可以通过调用next()throw()return()三种方法,将上下文执行环境重新加入到执行栈中,使得Generator函数的函数体代码重新恢复执行,此时Generator类实例对象的GeneratorStatus内部属性的值也会变为running
虽然Generator函数的函数体可以通过next()throw()return()三种方法重新开始执行,但是如果在函数体代码执行中遇到yield表达式,那么函数体代码将会重新暂停执行(再次将上下文执行环境冻结并移出执行栈),同时Generator类实例对象的GeneratorStatus内部属性的值也会重新变为suspended,只有再次使用next()throw()return()三种方法才能重新恢复代码执行。如此就可以通过yield表达式将一个整体的函数体代码执行分割成好几次执行。

function* generatorTest(){
	console.log('函数体代码第 1 段');
	yield;
	console.log('函数体代码第 2 段');
	yield;
	console.log('函数体代码第 3 段');
}
// Generator类 实例对象刚创建时 [[GeneratorStatus]]: "suspended"
var gt = generatorTest();
// Generator类 实例对象调用next方法,会去执行Generator函数 其中一段代码(执行时[[GeneratorStatus]]: "running"),
// 执行时遇到 yield表达式,所以会暂停代码执行,返回next方法的返回值。此时[[GeneratorStatus]]: "suspended"
gt.next(); // 控制台输出打印:函数体代码第 1 段
gt.next(); // 控制台输出打印:函数体代码第 2 段
gt.next(); // 控制台输出打印:函数体代码第 3 段

函数体代码终有执行结束的时候,如果函数体代码某次恢复执行后,所有函数体代码都执行完成,那么就代表了Genreator函数函数体执行完成,同时Generator类实例对象的GeneratorStatus内部属性的值也会变为closed,而且Generator类实例对象此时也不再可以通过next()throw()return()三种方法的调用去执行Genreator函数函数体。

function* generatorTest(){
	console.log('函数体代码第 1 段');
	yield;
	console.log('函数体代码第 2 段');
	yield;
	console.log('函数体代码第 3 段');
}
var gt = generatorTest();
gt.next(); // 控制台输出打印:函数体代码第 1 段
gt.next(); // 控制台输出打印:函数体代码第 2 段
gt.next(); // 控制台输出打印:函数体代码第 3 段
// 上次调用next后,函数体代码会执行完毕,所以 Generator 实例对象的内部属性的值会变为 closed 
gt;

总结:Generator类实例对象是一个控制Generator函数函数体代码执行的工具对象。

2.1函数体恢复执行

Generator函数在被调用时,其函数体代码是不会被执行的(代码此时不会执行,但是代码已经被解析过了,只等恢复后执行),只有通过Generator函数调用返回的Generator类实例对象调用next()throw()return()方法才能让函数体代码重新开始执行。

函数体代码执行会因为两种情况而暂停:

  • 一种就是在调用Generator函数时的暂停(调用Generator函数时,猜测JS解释器解析了函数体生成了属于函数的上下文执行环境,但是没有将执行环境加入到上下文执行环境栈中,所以这是一种暂停)
  • 另一种是函数体代码执行时碰到yield表达式而暂停(冻结属于函数的上下文执行环境,并将其移出上下文执行环境栈,所以这是另一种暂停)

2.1.1 next方法

next方法被调用时,会先将next方法的参数填充到函数体被暂停的地方,接着加上被填充的代码一起执行未执行过的函数体代码。

next方法恢复的是 因为调用Generator函数而暂停 的函数体时,那么next方法会在函数体顶部填充代码,接着恢复函数体执行。

function* generationFunction(){
	var print;
	var x;
	yield 1;
	console.log(111);
}
var g1 = generationFunction();
// 此时恢复执行的函数体是因为调用 Generator函数 而暂停的,所以next方法的参数会填充到函数体顶部,
// 所以执行了以下代码:{ 1111; var print; var x; yield 1 }
// 因为遇到了yield表达式,所以JS解释器暂停了代码执行
g1.next(1111); 

next方法恢复的是 因为遇到yield表达式而暂停 的函数体时,那么next会先将参数进行填充后,在恢复函数体的执行。

function* generationFunction(){
	var print;
	print = yield 1;
	console.log(yield 2);
}
var g1 = generationFunction();
// 参数因为是单独的执行,没有任何意义,所以第一次使用next恢复执行时,不用传递参数
g1.next();
// 此时恢复执行的函数体是因为遇到了 yield表达式 而暂停的,所以将next参数'输出打印'填充到上次暂停的位置后,在接着执行函数体代码。
// 所以执行了以下代码:{ print = '输出打印'; yield 2 } 
// 因为遇到了yield表达式,所以JS解释器暂停了代码执行
g1.next('输出打印');

2.1.2 throw方法

throw方法被调用时,JS解释器会使用throw关键字和throw方法的参数组合成一个抛出异常数据的代码,然后将这个组合后的代码填充到函数体被暂停的地方,接着加上被填充的代码一起执行未执行过的函数体代码。

throw方法恢复的是 因为调用Generator函数而暂停 的函数体时,那么函数体暂停的地方就是函数体的顶部,将组合代码填充在此处,接着恢复代码的执行,而由于函数体的顶部是没有任何其他的代码的,所以组合代码抛出的异常是必定无法被捕获的,所以会直接结束函数体代码的执行,并将异常会抛出到Generator函数外。

function* generationFunction(){
	var print;
	var x;
	yield 1;
	console.log(111);
}
var g1 = generationFunction();
// 此时恢复执行的函数体是因为调用 Generator函数 而暂停的,所以将组合代码填充到函数体顶部,
// 所以执行了以下代码:{ throw 1111; var print; var x; ... }
// 抛出的异常无法被捕获,所以下面的代码也就不能在继续执行了,函数体代码整体结束执行。
g1.throw(1111); 

throw方法恢复的是 因为遇到yield表达式而暂停 的函数体时,那么函数体暂停的地方就是上次代码执行时所遇到的yield表达式的位置,将组合代码填充在此处,接着恢复代码的执行,至于最后异常会不会被捕获,则看函数体代码的编写逻辑了。

function* generationFunction(){
	var print;
	print = yield 1;
	console.log(yield 2);
}
var g1 = generationFunction();
// JS解释器执行了{ var print; yield 1 }
g1.next();
// 此时恢复执行的函数体是因为遇到了 yield表达式 而暂停的,所以将组合代码填充到上次暂停的位置后,在接着执行函数体代码。
// JS解释器执行了{ print = throw 66666; }
// 抛出的错误没有被捕获,所以直接结束了函数体的执行,并在Generator函数外抛出了错误
g1.throw(66666); // Uncaught 66666

function* generationFunction(){
	try{
		var print;
		print = yield 1;
		console.log(yield 2);
	}catch(e){
		console.log(e);
		yield e;
	}
}
var g1 = generationFunction();
// JS解释器执行了{ var print; yield 1 }
g1.next();
// 此时恢复执行的函数体是因为遇到了 yield表达式 而暂停的,所以将组合代码填充到上次暂停的位置后,在接着执行函数体代码。
// JS解释器执行了{ print = throw 66666; },因为执行的代码在 try/catch 中,所以错误被捕获,
// 抛出错误下的{ console.log(yield 2); }代码不会被执行,而是直接执行 catch 中的代码:
// 接着JS解释器执行了{ console.log(e); yield e; }
// 因为遇到了yield表达式,所以JS解释器暂停了代码执行
g1.throw(66666);
// 因为上面填充进函数体的组合代码抛出的错误被捕获了,且函数体又再次被暂停了,所以可以继续恢复函数体执行
g1.next();

2.1.3 return方法

return方法被调用时,JS解释器会使用return关键字和return方法的参数组合成一个直接结束函数体执行的代码,然后将这个组合后的代码填充到函数体被暂停的地方,接着加上被填充的代码一起执行未执行过的函数体代码。

return方法恢复的是 因为调用Generator函数而暂停 的函数体时,那么函数体暂停的地方就是函数体的顶部,将组合代码填充在此处,接着恢复代码的执行,而由于函数体的顶部是没有任何其他的代码的,所以组合代码被执行后,会直接结束函数体代码的执行。

function* generationFunction(){
	var print;
	yield 1
	console.log(99999999);
}
var g1 = generationFunction();
// 此时恢复执行的函数体是因为调用 Generator函数 而暂停的,所以将组合代码填充到函数体顶部,
// 所以执行了以下代码:{ return 1111; var print; ... }
g1.return(11111); // 函数体代码中遇到了 return 代表直接结束函数体执行。

return方法恢复的是 因为遇到yield表达式而暂停 的函数体时,那么函数体暂停的地方就是上次代码执行时所遇到的yield表达式的位置,将组合代码填充在此处,接着恢复代码的执行,至于会不会直接结束函数体执行,则要看函数体代码逻辑编写了。

function* generationFunction(){
	var print;
	yield 1
	try{
		print = yield 2;
		console.log(yield 3);
	}catch(e){
		console.log(e);
		yield e;
	}finally{
		yield 4;
	}
	console.log(99999999);
}
var g2 = generationFunction();
// JS解释器执行了{ var print; yield 1 }
g2.next();
// 此时恢复执行的函数体是因为遇到了 yield表达式 而暂停的,所以将组合代码填充到上次暂停的位置也就是 yield 1 位置后,在接着执行函数体代码。
// JS解释器执行了{ return 333; try{ print = ... }
// 函数体代码中遇到了 return 代表直接结束函数体执行。
g2.return(333);

var g3 = generationFunction();
g3.next();
g3.next();
// 此时恢复执行的函数体是因为遇到了 yield表达式 而暂停的,所以将组合代码填充到上次暂停的位置也就是 yield 2 位置后,在接着执行函数体代码。
// JS解释器执行了{ print = return 111111; }但是这行代码在try中,且有finally存在,
// 那么JS解释器会将 执行完{ return 111111; }代码后 产生的结束函数体执行的效果进行延后,而是先去执行finally中的代码,
// 当finally中的代码执行完后,在恢复之前 结束函数体执行的效果
g3.return(111111);

总结:next()throw()return()三个方法本质上是同一个操作:都是在函数暂停的位置根据方法的参数填充代码,让后从填充的代码开始重写恢复函数体代码执行。只不过填充的代码因为调用哪个方法而各不相同。这种通过三种方法将Generator函数外部的数据填充到Generator函数内部的语法,有很重要的意义,Generator函数中的上下文执行环境中的所有变量的值都是由Generator函数函数中的执行代码来决定的,而如果通过三个方法的调用往函数体中填充代码数据,那么我们就可以使用Generator函数外部的数据,来影响Generator函数 上下文执行环境中的某段代码,而这段代码则可以影响Generator函数中的代码执行。

next()throw()return()调用时传递的参数是往Generator函数内部输入数据,而next()throw()return()的返沪指则是Generator函数向外输出数据

2.2 函数体中的关键字

next()throw()return()三个方法有返回值,返回值是一个对象,对象上有valuedone属性

next()throw()return()三个方法用来恢复函数体代码执行,而在代码执行时会遇到多种关键字,不同的关键字,会有不同操作效果,并且也会因为不同的操作效果而导致函数体代码是 暂停 还是 结束,而且在发生 暂停 或者 结束 时,也会根据操作效果来决定next()throw()return()三个方法的返回值对象上的valuedone属性的值

Generator函数的函数体代码恢复执行时,遇到yieldthrowreturnfinallythis等关键字JS解释器会进行不同的操作。

2.2.1 函数体中的yield

yield关键字只能在Generator函数中使用,用在其他地方会报错。

function* generatorFunction(){
	setTimeout(function(){
		// yield 在延时函数中使用,但是延时函数不是Generator函数,所以报错
		yield 1; 
	},1000);
}

在使用yield关键字时,yield关键字和其后面的表达式会组成一个yield表达式
yield表达式如果是存在与其他表达式内部的,那么必须用()括起来,否则报错,赋值表达式除外,但是最好统一用括号括起来。

(function*(){
	console.log(1 + yield 111);
})().next(); // Uncaught SyntaxError: Unexpected identifier
(function*(){
	console.log(1 + (yield 111));
})().next(); // {value: 111, done: false}
(function*(){
	let value = yield 111;
})().next(); // {value: 111, done: false}
(function*(){
	let value = (yield 111);
})().next(); // {value: 111, done: false}

通过next()throw()return()方法恢复的函数体代码执行如果遇到yield关键字时,JS解释器的操作,会先执行yield关键字后面的表达式,获取到表达式执行后的值,这个值我们此处称为值Y,而没有表达式的情况下值Y直接为undefined,接着暂停函数体剩余代码执行,最后将值Y作为next()throw()return()方法返回值对象的value属性的值,并且返回值对象的done属性的值是false

function* generatorFunction(){
	console.log('函数体第一段');
	yield 111;
	try{
		console.log('函数体第二段');
		yield 222;
		throw 333;
	}catch(e){
		console.log('函数体第三段');
		yield e;
	}finally{
		console.log('函数体第四段');
		yield 444;
	}
	console.log('函数体第五段');
	yield
}
var g = generatorFunction();
// JS解释器执行了「 console.log('函数体第一段'); yield 111; 」因为遇到yield,所以暂停并返回yield后面的表达式的值
g.next(); // {value: 111, done: false}
// JS解释器执行了「 66 try{ console.log('函数体第二段'); yield 222; 」因为遇到yield,所以暂停并返回yield后面的表达式的值
g.next(66); // {value: 222, done: false}
// JS解释器执行了「 77 throw 333;}catch(e){console.log('函数体第三段');yield e; 」因为遇到yield,所以暂停并返回yield后面的表达式的值
g.next(77); // {value: 333, done: false}
// JS解释器执行了「 88}finally{console.log('函数体第四段');yield 444; 」因为遇到yield,所以暂停并返回yield后面的表达式的值
g.next(88);// {value: 444, done: false}
// JS解释器执行了「 99}console.log('函数体第五段');yield; 」因为遇到yield,所以暂停并返回yield后面的表达式的值
g.next(99);// {value: undefined, done: false}

如果需要用到yield表达式的返回值时,在代码中最好能用一个临时变量来接收yield表达式的返回值,以便与理清代码逻辑

2.2.2 函数体中的throw

通过next()throw()return()方法恢复的函数体代码执行遇到throw关键字,或者遇到代码异常的时候,如果错误被捕获了,那就继续执行函数体代码,但是如果错误没有被捕获,那么将会直接结束函数体代码执行,且next()throw()return()方法不会有返回值

function* generatorFunction(){
	uuu + 1;
	yield 1;
}
// JS解释器恢复函数执行时遇到 uuu + 1; 代码出现错误,接着抛出错误,但没有try/catch捕获
generatorFunction().next(); // Uncaught ReferenceError: uuu is not defined

function* generatorFunction(){
	yield 1;
}
var g = generatorFunction();
g.next();
// JS解释器恢复函数执行时遇到 throw 111; 代码出现错误,接着抛出错误,但没有try/catch捕获
g.throw(11111); // Uncaught 11111

function* generatorFunction(){
	try{
		yield 1;
	}finally{
		uuu + 1;
	}
}
var g = generatorFunction();
g.next();
// 即使已将执行了return语句,但是如果在finally中抛出了错误,且没有被捕获的话,那么之前执行的return语句就会被抵消
// JS解释器恢复函数执行「 return 123123;}finally{uuu+1} 」时遇到 uuu+1; 代码出现错误,接着抛出错误,但没有try/catch捕获
g.return(123123); // Uncaught ReferenceError: uuu is not defined

function* generatorFunction(){
	try{
		yield 1;
	}finally{
		// 前面如果执行了return语句,这里抛出的错误也别捕获了,所以还是按照return语句来。
		try{uuu + 1;}catch(e){};
	}
}
var g = generatorFunction();
g.next();
g.return(111111); // {value: 111111, done: true}

function* generatorFunction(){
	throw 1111;
}
generatorFunction().next(); // Uncaught 1111

2.2.3 函数体中的return

通过next()throw()return()方法恢复的函数体代码执行遇到return关键字时,如果return表达式不在try/catch/finally中,JS解释器的操作,会先执行return关键字后面的表达式,获取到表达式执行后的值,这个值我们此处称为值R,而没有表达式的情况下值R直接为undefined,然后结束函数体代码执行,在结束函数体代码执行的同时,拿值R作为next()throw()return()方法返回值对象的value属性的值,并且返回值对象的done属性的值是true

function* generatorFunction(){
	yield 111;
	return 222+333;
	try{}catch(e){}finally{}
}
var g = generatorFunction();
g.next(); // {value:111, done:false}
// 「return 222+333;」下面的代码忽略,因为已经奇数函数体代码执行。
g.next(); // {value:555, done:true}

function* generatorFunction(){
	yield 111;
	try{}catch(e){}finally{}
}
g.next(); // {value:111, done:false}
// JS解释器执行了「666;try{}catch(e){}finally{};return;」
// 在函数体代码末尾是没有return语句的,但是JS解释器在执行时可以认为默认执行「return;」
g.next(666);// {value:undefined, done:true}

如果return表达式try/catch/finally的某个代码块中,那么JS解释器会先执行return关键字后面的表达式,获取到表达式执行后的值,这个值我们此处称为值R,而没有表达式的情况下值R直接为undefined,冻结值R,然后在某次代码恢复执行时,如果将finally中的代码执行完,那么会直接结束函数体代码执行,在结束函数体代码执行的同时,将前面冻结的值R作为next()throw()return()方法返回值对象的value属性的值,并且返回值对象的done属性的值是true

// return 语句在 try 代码块中,且存在 finally 代码块,那必须先将 finally 代码块中的代码执行完
function* generatorFunction(){
	var rv = 'try';
	console.log('函数体第一段');
	yield 111;
	try{
		return rv;
	}catch(e){
		return e;
	}finally{
		// 即使在此更改值,也不会影响最后的结果
		rv = 123;
		console.log('开始执行finally');
		console.log('finally将于执行结束');
	}
	console.log('......');
}
var g = generatorFunction();
// JS解释器执行:「var rv = 'try';console.log('函数体第一段');yield 111;」
g.next();
// JS解释器执行:「555;try{return rv;}finally{rv = 123;console.log('开始执行finally');console.log('finally将于执行结束');}」
g.next(555); // {value: "try", done: true}

// return 语句在 catch 代码块中,存在 finally 代码块,那必须先将 finally 代码块中的代码执行完
function* generatorFunction(){
	console.log('函数体第一段');
	yield 111;
	try{
		throw 'catch';
	}catch(e){
		return e;
	}finally{
		console.log('开始执行finally');
		console.log('finally将于执行结束');
	}
	console.log('......');
}
var g = generatorFunction();
// JS解释器执行:「console.log('函数体第一段');yield 111;」
g.next();
// JS解释器执行:「555;try{throw 'catch';}catch(e){return e;}finally{console.log('开始执行finally');console.log('finally将于执行结束');}」
g.next(555); // {value: "catch", done: true}

// return 语句在 finally 代码块中,那可以直接结束函数体代码执行,不必理会finally剩下的代码
function* generatorFunction(){
	console.log('函数体第一段');
	yield 111;
	try{
		throw 'catch';
	}catch(e){
	}finally{
		console.log('开始执行finally');
		return 'finally'
		console.log('finally将于执行结束');
	}
	console.log('......');
}
var g = generatorFunction();
// JS解释器执行:「console.log('函数体第一段');yield 111;」
g.next();
// JS解释器执行:「555;try{throw 'catch';}catch(e){}finally{console.log('开始执行finally');return 'finally';}」
g.next(555); // {value: "finally", done: true}

// 如果执行了两次 return语句 ,那以最新执行的 return语句 为准
function* generatorFunction(){
	console.log('函数体第一段');
	yield 111;
	try{
		return 'try';
	}catch(e){
	}finally{
		console.log('开始执行finally');
		return 'finally'
		console.log('finally将于执行结束');
	}
	console.log('......');
}
var g = generatorFunction();
// JS解释器执行:「console.log('函数体第一段');yield 111;」
g.next();
// JS解释器执行:「555;try{return 'try';}catch(e){}finally{console.log('开始执行finally');return 'finally';}」
g.next(555); // {value: "finally", done: true}

2.2.4 函数体中的this

Generator函数的函数体中this关键字都指向调用Generator函数的对象。

function* generatorFunction(obj){
	console.log('this关键字指向调用函数的对象:' + (this === obj));
}

// 调用函数的是全局对象
var g1 = generatorFunction(window);
g1.next(); // 控制台输出打印:this关键字指向调用函数的对象:true

var obj1 = {a:generatorFunction};
// 调用函数的是 obj1对象
var g2 = obj1.a(obj1);
g2.next(); // 控制台输出打印:this关键字指向调用函数的对象:true

var obj2 = {};
// 调用函数的是 obj2对象
var g2 = generatorFunction.call(obj2,obj2);
g2.next(); // 控制台输出打印:this关键字指向调用函数的对象:true

2.3 函数体的结束

Generator函数的函数体在经过next()throw()return()方法的循环恢复执行后,在三种情况下会结束执行:

  • 函数体代码执行时抛出了未捕获的错误,本次使得函数体代码恢复执行并结束的方法不会有返回值
  • 函数体代码执行时遇到了return语句,本次使得函数体代码恢复执行并结束的方法的返回值对象的value属性的值是return关键字后面的表达式的值。
  • 所有的函数体代码都执行完了本次使得函数体代码恢复执行并结束的方法的返回值对象的value属性的值是undefined

Generator函数的函数体在结束执行时,Generator类的实例对象的内部属性GeneratorStatus的值将变为closed,而且在内部属性GeneratorStatus的值为closed的情况下再次调用next()throw()return()方法:

function* generatorFunction(obj){
}
var g1 = generatorFunction();
g1.next();
g1; // generatorFunction {}
// 此时调用next和return 返回值恒定是 {value: undefined, done: true}
g1.next();
g1.return();
// 此时调用throw 直接在Generator函数外抛出错误
g1.throw(11111); // Uncaught 11111

总结:Generator类实例对象在函数体执行结束前通过next方法不断获取一个对象值,我们可以将这个对象值当成是函数体运行时的一种状态,如此在函数体执行结束前,我们可以将函数体分割为不同的状态运行。

Generator函数是容器,Generator类实例对象则是容器中内容的控制器

余留部分

Generator函数的简写

Generator函数定义时的简写方式(注意简写方法只能在给对象定义Generator函数时使用):
省略function关键字,但是留下*,使用简写定义后,Generator函数的函数名与对象属性名为同一个

var obj = {
	* generatorFunction(){}
}

yield* 表达式

yield*后面必须跟着实现了Iterable接口的对象,否则会报错:

function* generatorFunction(){
	// 当执行 yield* 表达式的时候,JS解释器先执行 yield* 后面的表达式获取值,
	// 然后调用值的 Symbol.iterator 属性方法,如果没有这个属性方法就会报错。
	yield* 1;
}
var g = generatorFunction();
g.next();

在执行Generator函数函数体代码遇到 yield* 表达式时,可以用while循环进行等价理解。
for...of循环无法获得return语句的返回值。

var arr = [0,1,2,3,4,5,6,7,8,9];
function* gf(){
	yield 'a';
	yield 'b';
	yield 'c';	
	return 'HeHe'
}
function* generatorFunction(){
	yield* arr;
	let returnValue = yield* gf();
}
// 上下两个Generator函数效果等价
function* generatorFunction(){
	let iterable1 = arr[Symbol.iterator]();
	let nextValue1 = iterable1.next();
	while(!nextValue1.done){
		yield nextValue1.value;
		nextValue1 = iterable1.next();
	}
	
	let iterable2 = gf()[Symbol.iterator]();
	let nextValue2 = iterable2.next();
	while(!nextValue2.done){
		yield nextValue2.value;
		nextValue2 = iterable2.next();
	}
	let returnValue = nextValue2.value;
}

Generator函数应用

1、异步操作的同步化表达

核心思想:将 开启异步操作的代码 与 处理异步结果的代码 都放在 Generator函数 的函数体中,并用 yield表达式 将两者隔开,如此在 Generator函数 中,光看代码上下顺序就有了一种同步化的感觉。

// 网络请求 时 加载的界面
function* networkRequestLoadingUI(){
	// 下面yield表达示之前的代码就是 开启异步操作的代码
	showLoadingUI();
	networkRquest(function resultCallBack(response){
		loadingUI.next(); // 网络请求有结果后,关闭加载界面
	});
	yield;
	// 下面的代码就是 处理异步结果的代码
	closeLoadingUI();
}
var loadingUI = networkRequestLoadingUI();
// 执行 开启异步操作的代码
loadingUI.next(); // 显示加载界面并开启网络请求

// 网络请求数据的Generator函数
function* networkRequestData(){
	// 下面yield表达示之前的代码就是 开启异步操作的代码
	var settings = {
  		"async": true,
  		"crossDomain": true,
  		"url": "https://www.baidu.com/",
  		"method": "GET",
  		"headers": {
    		"cache-control": "no-cache",
    		"postman-token": "86087ec3-4666-c7b8-b711-d7144ad9e6ab"
  		}
	}
	$.ajax(settings).done(function (response) {
		requestData.next(response);
	});
	// 收到网络请求后的数据
	let requestResult = yield 'start_request';
	// 下面的代码就是 处理异步结果的代码
	// ... 对requestResult这个请求结果进行处理
}
var requestData = networkRequestData();
// 执行 开启异步操作的代码
requestData.next();

2、控制流管理

核心思想:如果有一组功能代码,它可以被分割成很多步骤,我们将每一步骤单独独立出来,然后按照顺序执行步骤代码,也可以实现先前的功能,而在执行步骤代码的前后我们有时可能需要穿插一些别的代码进行判断或者操作,这些判断或者操作有可能会影响到下一步步骤代码的执行。此时我们可以将一组功能的所有步骤代码放入Generator函数中,然后用yield表达式插入到每个步骤代码之间。如此我们就可以通过Generator类的实例对象去控制一组功能中的每个步骤代码什么时候执行,怎样执行。注意这里的代码流是同步执行的,异步执行在这里不适用。

// 一组编写代码的功能
function writeCode(){
	var s1 = '打开电脑';
	console.log('打开电脑');
	var s2 = s1 + ' - 打开编辑器'
	console.log('打开编辑器');
	var s3 = s2 + ' - 编写代码';
	console.log('编写代码');
	var s4 = s3 + ' - 调试代码'
	console.log('调试代码');
	var s5 = s4 + ' - 代码上线'
	console.log('代码上线');
	console.log(s5);
}
// 分割成多个步骤代码
function step1(){
	var s1 = '打开电脑';
	console.log('打开电脑');
	return s1;
}
function step2(value){
	var s2 = value + ' - 打开编辑器'
	console.log('打开编辑器');
	return s2;
}
function step3(value){
	var s3 = value + ' - 编写代码';
	console.log('编写代码');
	return s3;
}
function step4(value){
	var s4 = value + ' - 调试代码'
	console.log('调试代码');
	return s4;
}
function step5(value){
	var s5 = value + ' - 代码上线'
	console.log('代码上线');
	return s5;
}

// 使用Generator函数控制多步骤执行
function* generator(){
	let result1 = yield step1();
	let result2 = yield step2(result1);
	let result3 = yield step3(result2);
	let result4 = yield step4(result3);
	let result5 = yield step5(result4);
	console.log(result5);
}

// 编写一个具体控制Generator函数函数体代码执行的函数
// 控制Generator函数函数体代码执行 也就是 控制步骤代码的执行
function writeCode(){
	var g = generator();
	
	var nextvalue = g.next();
	while(!nextvalue.done){
		// 比如此处 在进行下一步步骤代码执行前需要先判断一下数据
		if(nextvalue !== undefined){
			nextvalue = g.next(nextvalue.value);
		}else{
			return;
		}
	}	
}

3、部署Iterable接口

核心思想:可以将任何数据作为Generator函数的参数,当我们调用Generator函数返回的Generator类实例对象就可以认为是参数的遍历器对象,且这个遍历器对象上还有Symbol.iterator属性方法,方法的返回值指向遍历器对象自己。

// 任意对的自身属性便器对象生成函数
function* objGenerator(obj){
	var attrs = Reflect.ownKeys(obj);
	for(let attrName of attrs){
		yield [attrName, obj[attrName]];
	}
}

let myObj = { foo: 3, bar: 7 };
for (let [key, value] of objGenerator(myObj)) {
  console.log(key, value);
}

协程的思想来理解Generator函数:参考Generator函数深入应用

参考博客:

  1. http://es6.ruanyifeng.com/#docs/generator

你可能感兴趣的:(JavaScript)