Generator 函数的语法

Generator


基本概念
  • 形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();
hw.next()   // { value: 'hello', done: false }
hw.next()   // { value: 'world', done: false }
hw.next()   // { value: 'ending', done: true }
hw.next()   // { value: undefined, done: true }

//上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield
//表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句
//(结束执行)。
  • 如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
let obj = {
  * myGeneratorMethod() {
    ···
  }
};

Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator)

  • 下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

  • 总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。


yield表达式
  • yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
  • 由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志
遍历器对象的next方法的运行逻辑如下。
(1)遇到 yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的 value属性值
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到 return语句为止,并将return语句后面的表达式的值,作为返回的对象的 value属性值
(4)如果该函数没有return语句,则返回的对象的 value属性值为undefined
// 下面是一个 yield 应用的例子
var arr = [1, [[2, 3], 4], [5, 6]];

// 重新定义一个新的平铺函数,即将嵌套的数组全部拆开 => arr.flat(n)
var flat = function* (a) {
  var length = a.length;
  for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== 'number') {
     // 抛出一个 Generator 函数
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)) {
  console.log(f);
}
// 1, 2, 3, 4, 5, 6
  • 另外,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 接口的关系
  • 由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]
// 此时的 myIterable 具备 iterator部署了接口,可以被扩展运算符遍历

next 方法的参数
  • yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
// 对比理解的例子:
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}
// 出现 NAN 的原因是因为 yield的返回值为undefined,2*undefined=undefined

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
// 这里的12 替代了yield(x+1),所以 y= 24

b.next(13) // { value:42, done:true }
// 这里的13替代了 yield(y/3),所以 z= 13

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。


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
// 没有出现 6 是因为 for...of循环有一个规则就是当 done=true的时候即刻停止
// 且不返回该对象的 value属性

利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for…of循环,通过Generator函数为它加上这个接口,就可以用了。

// 这就可以解决我们之前 Iterator 存在的问题
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);
  // proKeys => obj对象包含属性的数组 ['first', 'last']
  // Reflect.ownKeys()方法返回一个由目标对象自身的属性键组成的数组。
  
  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

除了for...of循环以外,扩展运算符(...)解构赋值Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 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 函数体内捕获。
  • throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例
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语句已经执行过了,
//不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数
//体外的catch语句捕获。

如果 Generator函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

var g = function* () {
 	//里面不做 try...catch 错误处理
};

var i = g();
i.next();

try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 外部捕获 a
// 抛出的第二个错误在第一个错误抛出时已经被中断了,不会再执行错误抛出
总结:
  1. throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
  1. throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
  1. 只要部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。
function* IndexError(){
    let index = 0,
        arr = [1, 2, 3]
    try{
        yield arr
    }catch(err){
        console.log(`${err} => 内部错误`)
    }
    yield ++index
}

let Something = IndexError()
Something.next()  // {value:[1, 2, 3], done:false}
try{
    Something.throw (new Error(`Error`))
}catch(err){
    console.log(`${err} => 外部错误`)   //{value:1, done:false}
}
Something.next()           // {value:undefined, done:true}

Generator.prototype.return()
function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();
g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }

//遍历器对象g调用return方法后,返回值的value属性就是return方法的参数foo。
//并且,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用
//next方法,done属性总是返回true。

//如果return方法调用时,不提供参数,则返回值的value属性为undefined。
  • 如果 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 }
g.next()  //{value:undefined, done:true}

//上面代码中,调用return()方法后,就开始执行finally代码块,不执行try里面
//剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回
//值。

yield* 表达式

yield*表达式用来在一个 Generator 函数里面执行另一个 Generator 函数。

// 例子 bar函数内部调用 foo函数
function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}
// 省去了遍历 foo 函数的步骤,让代码更加得通俗易懂
// 我们也可以这么理解:yield*后面的 Generator 函数(没有return语句时),
//等同于在 Generator 函数内部,部署一个for...of循环。

如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员

function* gen(){
  yield* ["a", "b", "c"];
}

gen().next() // { value:"a", done:false }
gen().next() // { value:"b", done:false }
gen().next() // { value:"c", done:false }
gen().next() // { value:"undefined", done:false }

不仅仅是数组,实际上任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

let read = (function* () {
  yield 'hello';
  yield* 'hello';
})();

// 对比两次的 next也可以证实我们上面的结论
read.next().value // "hello"
read.next().value // "h"

Generator 函数的 this
  • Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
function* g() {}

g.prototype.hello = function () {
  return 'hi!';
};

let obj = g(); // obj 继承了 g.prototype

obj instanceof g // true
obj.hello()     // 'hi!'
但是,如果把 Generator函数当作普通的构造函数,并不会生效,因为Generator返回的总是遍历器对象,而不是 this对象
(简单来说就是他的实例并不是一个正常的对象而是一个遍历器对象, this不指向遍历器对象)。
function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a // undefined
  • Generator 函数也不能跟new命令一起用,会报错。
function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F()
// TypeError: F is not a constructor

解决的办法:

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
  // 将 gen()函数的 this指向 gen()的原型对象
}

var f = new F();
// 此时实例化F() 等同于实例化 gen()的原型对象

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值