Generator 函数的语法
基本介绍
Genertator 函数是ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解的角度,语法上, 首先可以把它理解成,Genterator函数是一个状态机,封装了多个内部状态。
执行Generator 函数会返回一个遍历器对象, 也就是说, Genertator 函数除了状态机,还是一个遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
形式上,Generator函数是一个普通函数, 但是有两个特征,一是,function关键字与函数名之间有一个星号;二是, 函数体内部使用yield 表达式 , 定义不同的内部状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个Generator 函数 , 它内部有两个yield表达式 ,即刻函数有三个状态:hello , world 和return语句。
然后, Generator 函数的调用方法与普通函数一样, 也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后 ,函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的的一个指针对象,也就是遍历器对象。
下一步,必须调用遍历器对象的next 方法, 使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或者上一次停下来的地方开始执行, 直到遇到下一个yield 表达式,为止,换言之。Generator函数是分段执行的。yield表达式是暂停执行的的标记,而next 方法可以恢复执行。
总结
调用Generator 函数, 返回一个遍历器对象, 代表Generator函数的内部指针。以后, 每次调用遍历器对象的next 方法,就会返回一个有着value 和 done两个属性值的对象。value 属性表示当前内部状态的值。 是yield表达式后面的那个表达式的值;done 属性是一个布尔值, 表示是否遍历结束。yield 表达式
由于Generator 函数返回的遍历器对象,只有调用next 方法才会遍历下一个内部状态,所以其实提供了一种可暂停执行的函数, yield 表达式就是暂停的标志。
遍历器对象的next 方法的运行逻辑如下
(1) 遇到yield表达式,就暂停执行后面的操作, 并将紧跟在yield 后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有遇到新的yield 表达式, 就一直运行到函数结束, 直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果函数没有return语句,则返回的对象的value 属性值为undefined
需要注意的是,yield 表达式后面的表达式,只有当调用next 方法,内部指针指向该语句时才会执行,因此类似于惰性取值。
function* gen() {
yield 123 + 456;
}
上面的代码中 ,yield 后面的表达式123+456 不会立即求值, 只会在next方法将指针移到这一句是才会求值。
yield表达式和return语句既有相似之处 , 也有区别,相似之处在于, 都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield , 函数暂停执行, 下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能, 一个函数里面只能执行一次return语句, 但是可以执行多次yield表达式。正常的函数只能返回一个值,因为只能执行一次return;generator函数可以返回一系列的值,因为可以有任意多的yield 从另一个角度看, 也可以说Generator生成了一系列的值。
注意 yield 关键字只能用在Generator函数里面,用在其他地方会报错,!!!!!
Generator 函数可以不用yield 表达式,这时就变成了一个单纯的暂缓执行的函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f如果是普通函数,在为遍历generator赋值时就会执行。但是 , 函数f是一个generator函数, 就变成只有只有调用next 方法时,函数f才会执行。
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* 表示可以调用Generator函数。上面的代码把一个复杂的数组对象给化解了
另外,yield 表达式如果用在另一个表达式中,必须方法圆括号里。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
与Iterator接口的关系
上一章说过,任意一个对象的Symbol.iterator方法 , 等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于Generator 函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
上面代码中,Generator 函数赋值给Symbol.iterator属性, 从而使得myIterable对象具有了Iterator接口,可以被…运算符遍历了。
Generator函数执行后,返回一个遍历器对象,该对象本身也具有Symbol.iterator 属性, 执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面代码中,gen是一个Generator函数, 调用它会生成一个遍历器对象G ,它的Symbol.iterator属性,也是一个遍历器生成函数,执行后返回它自己 。
next 方法的参数
yield 表达式本身没有返回值,或者说总是返回undefined 。 next 方法可以带一个参数,该参数就会被当作上一个yield 表达式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面的代码先定义一个可以无限循环的Generator函数 f ,如果next 方法没有参数,每次运行到yield 表达式,变量reset 的值总是undefined 。 当next 方法带了一个参数true时, 变量reset就被重置为这个参数因此i 会等与 -1 下一轮循环就从-1开始递增。
这个功能有很重要的语法意义,Generator 函数从暂停状态到恢复运行,它的上下文状态是不变的。通过next方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在Generator函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
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方法的参数表示上一个yield 表达式的返回值, 所以第一次使用next 方法时, 传递参数是无效的,V8引擎直接忽略第一次使用next 方法时的参数,只有从第二次使用next 方法开始,参数才是有效的。从语义上讲,第一个next 方法用来启动遍历器对象,所以不用带有参数。
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 循环
for…of 循环可以自动循环遍历 Generator 函数执行时生成的Iterator对象, 且此时不再需要调用next 方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
上面代码使用for…of循环 , 依次显示5个yield表达式的值。这里需要注意,一旦next 方法的返回对象的done 属性为true ,for…of循环就会中止,且不包含该返回对象,所以上面的代码return 语句返回的6 不包括在for…of 循环之中。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 1000) break;
console.log(n);
}
for 循环无限循环 , 每次遇到yield 就返回,因为for循环是无限循环 ,所以第一次是1 , 第二次的时候上一个数加当前这个数,所以第二个数是1 第三个数是2 第四个数是3
利用Genterator 遍历对象
function* objectTest(obj){
// 获取对象中的所有的属性名,并通过数组返回
let propkeys = Reflect.ownKeys(obj);
console.log(propkeys);
for(let propKey of propkeys){
yield[propKey , obj[propKey]];
}
}
let name = {"id":1,"name":'admin'};
for(let [key,value] of objectTest(name)){
console.log(key,value);
}
上面的代码中,对象 name 原生不具备Iterator 接口, 无法用 for…of 遍历, 这时, 我们通过Generator 函数为他加上遍历器接口,就可以用for…of遍历了,加上遍历接口的另一种写法是, 将Generator函数加到对象的Symbol.iterator属性上面。
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 方法内部调用的,都是遍历器接口。这意味着。他们都可以将Gnerator 函数返回的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.prototype.throw()
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
上面代码中, 遍历器对象i 连续抛出两个错误,第一个错误被Generator 函数体内catch语句捕获, i第二次抛出错误,由于Generator函数内部catch 语句已经执行过了,不会再捕获这个错误了,所以这个错误被抛出了函数体完,被体外的catch捕获。
throw 方法可以接受一个参数, 该参数会被catch语句接收, 一般建议抛出Error对象的实例
var g = function* () {
try {
yield;
} catch (e) {
console.log(e);
}
};
var i = g();
i.next();
i.throw(new Error('出错了!'));
// Error: 出错了!(…)
注意, 不要混淆遍历器对象的throw方法和全局的throw 命令,上面代码的错误,是用遍历器对象的throw方法抛出的,而不是throw命令抛出的,后者只能被函数体完的catch语句捕获。
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
};
var i = g();
i.next();
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
上面代码之所以捕获了a是因为函数体外的catch 语句块, 捕获了抛出的a错误以后就不会再继续try代码块里面剩余的语句。
如果Generator 函数内部没有部署try… catch 代码块,那么throw 方法抛出的错误,将被部署在外部的的tey 代码捕获。
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
上面代码中,Generator 函数G 内部没有部署tey…chatch 代码块。所以抛出的错误直接被外部catch代码块捕获。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代码 中,g.throw 抛出错误后, 没有任何try…catch 代码块可以捕获这个错误, 导致程序出错。
throw 方法抛出的错误要被内部捕获, 前提是必须至少在执行一个next方法。