Generator函数之基本用法
(1)基本概念
Generator函数是ES6 提供的一种异步编程解决方案,语法与传统函数完全不同。
Generator函数与普通函数在写法上的不同
1.function命令与函数名之间有一个星号(*)。
2.函数体内部使用yield语句定义不同的内部状态。
Generator函数的调用方法
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一个圆括号。但是,调用Generator函数后,这个函数并不会执行。返回的是一个指向内部状态的指针对象,也就是遍历器对象。
接下来必须调用遍历器对象的next方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一条yield语句(或return语句为止)。换言之,Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
function* generator() {
yield "q";
yield "w";
return "e";
}
let a = generator();
console.log(a.next());
//{value:"q",done:false}
console.log(a.next());
//{value:"w",done:false}
console.log(a.next());
//{value:"e",done:true}
console.log(a.next());
//{value:undefined,done:true}
1.调用Generator函数返回一个遍历器对象,代表Generator函数的内部指针。以后每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。
value表示当前的内部状态的值,是yield语句后面的表达式的值(如果yield后面没有值,value属性的值就是undefined);done属性是一个布尔值,表示是否遍历结束。
2.如果遇到return语句,则value属性的值是return语句后面的表达式的值(如果return语句后面没有值,value属性的值就是undefined),done属性的值为true,表示遍历结束。
3.如果done属性变为true后继续调用next方法,返回的value属性的值为undefined,done属性的值为true。
(2)yield表达式
Generator函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以说Generator函数是一种可以暂停执行的函数。yield就是暂停执行的标志。
1.遍历器对象的next方法的运行逻辑:
1.遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
2.下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
3.如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
4.如果该函数没有return语句,则返回的对象的value属性值为undefined。
2.惰性求值
只有调用next方法且内部指针指向该语句时才会执行yield语句后面的表达式。
function* generator() {
yield 1 + 2;
}
上面的代码中,yield后面的表达式1+2不会立即求值,只会在next方法将指针移到这一句时才会求值。
3.yield语句与return语句
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。
4.Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
5.yield表达式只能用在 Generator 函数里面
yield表达式如果用在其他函数中就会报错。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
上面的代码中,forEach方法的参数是一个普通函数,但是在里面使用了yield表达式(也使用了yield*表达式),所以会产生句法错误。
6.yield表达式如果用在另一个表达式之中必须放在圆括号中
function* generator() {
console.log("ni hao " + (yield 123));
console.log("hello " + (yield 456))
}
let a = generator();
console.log(a.next());
//{value:123 done:false}
console.log(a.next());
// ni hao undefined
//{value:456 done:false}
console.log(a.next());
// hello undefined
//{value:undefined done:true}
console.log(a.next());
//{value:undefined done:true}
yield表达式用作函数的参数或者放在赋值表达式的右边,可以不加括号。
(3)与Iterator接口的关系
任意一个对象的Symnol.iterator方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的遍历器对象。
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。
let demo = {};
demo[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
let a=[...demo];
console.log(a);
//[1,2,3]
for(let item of demo){
console.log(item);
}
//1
//2
//3
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。
(4)next方法的参数
yield语句本身没有返回值。next方法可以带一个参数,这个参数会被当作上
一条yield语句的返回值。
function* generator() {
let b=yield "q";
let a=b/2;
console.log(a);
yield "w";
return "e";
}
let a = generator();
console.log(a.next());
console.log(a.next(2))
//{value:q,done:false}
//1
//{value:w,done:false}
第二次调用next方法的时候,传入了参数2,因为next方法传入的参数被当作上一条yield语句的返回值,所以这是b的值为2,之后a=b/2,a的值为1,之后打印出1。
通过next方法的参数,可以在Generator函数运行的不同阶段从外部向内部注入不同的值,从而可以调整函数的行为。
注意第一次使用next方法时传递参数是无效的:因为next方法的参数表示上一条yield语句的返回值,所以第一次使用next方法时传递的参数是无效的。
(5)for…of循环
for…of循环可以自动遍历Generator函数返回的遍历器对象,此时不再需要调用next方法:
function* generator() {
yield "q";
yield "w";
return "e";
}
for(let item of generator()){
console.log(item)
}
//q
//w
一旦next方法的返回对象的done属性为true,for…of循环就会终止,且不包含该返回对象,所以上面的return语句返回的"e"不包括在for…of循环中。
原生的普通对象没有遍历接口,无法使用for…of循环,通过Generator函数可以为它加上这个接口:
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
另外一种写法:
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for…of循环以外,扩展运算符(…)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的遍历器对象,作为参数。
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
(6)Generator.prototype.throw()
1.Generator函数返回的遍历器对象都有一个throw方法
可以在函数体外抛出错误,然后在Generator函数体内捕获。同时throw方法可以接受一个参数,这个参数会被catch语句接受。
function* generator() {
try {
yield;
} catch (e) {
console.log("内部捕获 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
//内部捕获 a
如果遍历器对象抛出两个错误,而Generator函数体内只有一个try/catch语句,则第二次抛出的错误不会被捕获:
function* generator() {
try {
yield;
} catch (e) {
console.log("内部捕获 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
i.throw("b");
//内部捕获 a
//Uncaught b
遍历器对象i抛出第一个错误,被Generator函数内部的catch语句捕获并处理,这是catch语句已经执行过了。之后i抛出第二个错误,但是因为Generator函数内部的catch语句已经执行过了,所以不会捕捉到这个错误。
如果遍历器对象有两个try/catch语句:
function* generator() {
try {
yield;
} catch (e) {
console.log("内部捕获1 " + e);
}
try {
yield;
} catch (e) {
console.log("内部捕获2 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
i.throw("b");
//内部捕获1 a
//内部捕获2 b
2.注意遍历器对象的throw方法与全局throw命令的不同
用遍历器对象的throw方法抛出的错误才会被Generator函数内部的catch语句捕获。而全局的throw命令抛出的错误只能被Genenrator函数体外的catch语句捕获。
function* generator() {
try {
yield;
} catch (e) {
console.log("内部捕获 " + e);
}
}
let i = generator();
i.next();
throw("a");
//Uncaught a
function* generator() {
try {
yield;
} catch (e) {
console.log("内部捕获 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
//内部捕获 a
try{
throw("b");
}catch(e){
console.log("外部捕获 "+e)
}
//外部捕获 b
函数外部的一个try/catch语句也是一次只能捕获一个错误:
try{
throw("a");
throw("b");
}catch(e){
console.log("外部捕获 "+e)
}
//外部捕获 a
3.如果Generator函数内部没有部署try/catch语句,则throw语句抛出的错误将被外部的try/catch语句捕获
function* generator() {
yield;
}
let i = generator();
i.next();
try {
i.throw("a");
} catch (e) {
console.log("外部捕获 " + e)
}
//外部捕获 a
4.throw方法对下一次遍历的影响
如果Generator函数没有部署try/catch语句,则调用throw方法会使遍历终止
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
i.throw("a");
//Uncaught a
如果Generator函数部署了try/catch语句,则调用throw方法不影响下一次遍历
throw方法被捕获之后会附带执行下一条yield表达式,也就是会附带执行一次next方法:
function* generator() {
try{
yield 123;
}catch (e) {
console.log("内部捕获 "+e)
}
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.throw("a"));
//内部捕获 a
//{value:456 done:false}
console.log(i.next());
//{value:789 done:false}
console.log(i.next());
//{value:undefined done:true}
(7)Generator.prototype.return()
Generator函数返回的遍历器对象还有一个return方法,可以返回给定的值,并终结Generator函数的遍历。
1.return方法带参数,返回值的value属性为参数值
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.return("a"));
//{value:"a" done:true}
console.log(i.next());
//{value:undefined done:true}
遍历器对象调用return方法后,返回值的value属性就是return方法的参数(这里是“a”)。同时Generator函数的遍历终止,返回值的done属性为true。以后再调用next方法,value属性的值总为undefined,done属性的值总为true。
2.如果return方法不提供参数,则返回值的value属性为undefined
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.return());
//{value:undefined done:true}
console.log(i.next());
//{value:undefined done:true}
3.return方法遇到Generator函数内部有try…finally代码块
如果 Generator 函数内部有try…finally代码块,且正在执行try代码块,那么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 }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。
如果 Generator 函数内部有try…finally代码块,但是现在并没有执行try代码块,那么return方法会直接执行,返回的值的value属性是return方法的参数,done属性是true。
function* numbers() {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
let g = numbers();
console.log(g.next());
//{value:1,done:false}
console.log(g.return(7));
//{value:7,done:true}
console.log(g.next());
//{value:undefined,done:true}