这是我参与8月更文挑战的第9天,活动详情查看:8月更文挑战
前情提要
前面我们讲了generator函数的定义以及函数的执行机制,接下来我们来学习一下generator函数的其他功能。
函数返回的遍历器对象的方法
next方法
一般情况下,next 方法不传入参数的时候,yield 表达式的返回值是 undefined 。当 next 传入参数的时候,该参数会作为上一步yield的返回值。
function* sendParameter(){
console.log("start");
var x = yield '2';
console.log("one:" + x);
var y = yield '3';
console.log("two:" + y);
console.log("total:" + (x + y));
}
//next不传参
var sendp1 = sendParameter();
sendp1.next();
// start
// {value: "2", done: false}
sendp1.next();
// one:undefined
// {value: "3", done: false}
sendp1.next();
// two:undefined
// total:NaN
// {value: undefined, done: true}
//next传参
var sendp2 = sendParameter();
sendp2.next(10);
// start
// {value: "2", done: false}
sendp2.next(20);
// one:20
// {value: "3", done: false}
sendp2.next(30);
// two:30
// total:50
// {value: undefined, done: true}
先看一个简单的小栗子,并尝试解析遍历生成器函数的执行过程
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next()) // {value: 14, done: false}
console.log(it.next()) // undefined {value: undefined, done: false}
}
第一次调用遍历器对象的next方法,函数从头部开始执行,遇到第一个 yield 暂停,在这个过程中其实是分了三步:
(1)、声明了一个变量result,并将声明提前,默认值为 undefined
(2)、由于 Generator函数是 “惰性求值”,执行到第一个 yield 时才会计算求和,并加计算结果返回给遍历器对象 {value: 14, done: false},函数暂停运行
(3)、理论上应该要把等号右边的 [yield 3 + 5 + 6] 赋值给变量result,但是,由于函数执行到 yield 时暂定了,这一步就被挂起了
第二次调用next方法,函数从上一次 yield 停下的地方开始执行,也就是给result赋值的地方开始,由于next()并没有传参,就相当于传参为undefined 基于以上分析,就不难理解为什么说 yield表达式本身的返回值(特指 [rv]) 总是undefined了。现在把上面的代码稍作修改,第二次调用 next() 方法传一个参数3,按照上图分析可以很快得出输出结果
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next()) // {value: 14, done: false}
console.log(it.next(3)) // 3 {value: 3, done: false}
}
如果第一次调用next()的时候也传了一个参数呢?这个当然是无效的,next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。
从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
{
function* gen() {
let result = yield 3 + 5 + 6
console.log(result)
yield result
}
let it = gen()
console.log(it.next(10)) // {value: 14, done: false}
console.log(it.next(3)) // 3 {value: 3, done: false}
}
Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
{
function* gen(x) {
let y = 2 * (yield (x + 1)) // 注意:yield 表达式如果用在另一个表达式中,必须放在圆括号里面
let z = yield (y / 3)
return x + y + z
}
let it = gen(5)
/* 通过前面的介绍就知道这部分输出结果是错误的啦
console.log(it.next()) // {value: 6, done: false}
console.log(it.next()) // {value: 2, done: false}
console.log(it.next()) // {value: 13, done: false}
*/
/*** 正确的结果在这里 ***/
console.log(it.next()) // 首次调用next,函数只会执行到 “yield(5+1)” 暂停,并返回 {value: 6, done: false}
console.log(it.next()) // 第二次调用next,没有传递参数,所以 y的值是undefined,那么 y/3 当然是一个NaN,所以应该返回 {value: NaN, done: false}
console.log(it.next()) // 同样的道理,z也是undefined,6 + undefined + undefined = NaN,返回 {value: NaN, done: true}
}
如果向next方法提供参数,返回结果就完全不一样了
{
function* gen(x) {
let y = 2 * (yield (x + 1)) // 注意:yield 表达式如果用在另一个表达式中,必须放在圆括号里面
let z = yield (y / 3)
return x + y + z
}
let it = gen(5)
console.log(it.next()) // 正常的运算应该是先执行圆括号内的计算,再去乘以2,由于圆括号内被 yield 返回 5 + 1 的结果并暂停,所以返回{value: 6, done: false}
console.log(it.next(9)) // 上次是在圆括号内部暂停的,所以第二次调用 next方法应该从圆括号里面开始,就变成了 let y = 2 * (9),y被赋值为18,所以第二次返回的应该是 18/3的结果 {value: 6, done: false}
console.log(it.next(2)) // 参数2被赋值给了 z,最终 x + y + z = 5 + 18 + 2 = 25,返回 {value: 25, done: true}
}
{
function* gen(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
z = 88 // 注意看这里
return x + y + z
}
let it = gen(5)
console.log(it.next()) // {value: 6, done: false}
console.log(it.next(9)) // {value: 6, done: false}
console.log(it.next(2)) // 这里其实也很容易理解,参数2被赋值给了 z,但是函数体内又给 z 重新赋值为88, 最终 x + y + z = 5 + 18 + 88 = 111,返回 {value: 111, done: true}
}
除了使用 next ,还可以使用 for... of 循环遍历 Generator 函数生产的 Iterator 对象。
return 方法
return 方法返回给定值,并结束遍历 Generator 函数。
return 方法提供参数时,返回该参数;不提供参数时,返回 undefined 。
function* foo(){
yield 1;
yield 2;
yield 3;
}
var f = foo();
f.next();
// {value: 1, done: false}
f.return("foo");
// {value: "foo", done: true}
f.next();
// {value: undefined, done: true}
throw 方法
throw 方法可以再 Generator 函数体外面抛出异常,再函数体内部捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('catch inner', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('catch outside', e);
}
// catch inner a
// catch outside b
遍历器对象抛出了两个错误,第一个被 Generator 函数内部捕获,第二个因为函数体内部的catch 函数已经执行过了,不会再捕获这个错误,所以这个错误就抛出 Generator 函数体,被函数体外的 catch 捕获。
yield* 表达式
yield* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部,调用另一个 Generator 函数。
function* callee() {
console.log('callee: ' + (yield));
}
function* caller() {
while (true) {
yield* callee();
}
}
const callerObj = caller();
callerObj.next();
// {value: undefined, done: false}
callerObj.next("a");
// callee: a
// {value: undefined, done: false}
callerObj.next("b");
// callee: b
// {value: undefined, done: false}
// 等同于
function* caller() {
while (true) {
for (var value of callee) {
yield value;
}
}
}
使用场景
实现 Iterator
为不具备 Iterator 接口的对象提供遍历方法。
function* objectEntries(obj) {
const propKeys = Reflect.ownKeys(obj);
for (const propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
const jane = { first: 'Jane', last: 'Doe' };
for (const [key,value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
Reflect.ownKeys() 返回对象所有的属性,不管属性是否可枚举,包括 Symbol。
jane 原生是不具备 Iterator 接口无法通过 for... of遍历。这边用了 Generator 函数加上了 Iterator 接口,所以就可以遍历 jane 对象了。
至此我们学习完了generator函数的所有应用方法。