Generator的语法

generator的英文意思是生成器

简介

关于Generator函数,我们可以理解成是一个状态机,里面封装了多种不同的状态。

function* gener(){
    yield 'hello';
    yield 'world';
    return 'ending'; 
}

var g = gener();   // g是一个遍历器对象
g.next();   // {value:'hello',done:false}
g.next();   // {value:'world',done:false}
g.next();   // {value:'ending',done:true}
g.next();   // {value:undefined,done:true}

上面代码定义了一个Generator函数,这个函数有两个地方与其他函数不同

  1. function后边跟一个*
  2. 函数内部有一个关键字yield。yield表示定义一个状态,所以上边的函数其实有三个状态。
  3. 调用Generator函数和普通函数一样,都是后面跟圆括号,不过调用Generator函数,函数并不执行,返回的也不是正常的返回结果,而是一个指向内部状态的指针函数,也就是Iterator这个遍历器对象。

来剖析一下上面的代码:

  • 上面代码一共执行了四次。调用next()便会执行一次。每次代码执行到yield的时候就会停下来,输出一个对象,对象的value值就是yield后边的值,done这个值表示遍历是否结束,这个时候为false。
  • 第二次同理,遇到了第二个yield函数会停下来,输出的遍历器对象value值为'world',done的值依然是false。
  • 第三次,当代码遇到了return语句(如果没有return语句,就一直执行到函数结束)这个时候返回的遍历器对象的value的值,就是return后边跟的表达式的值,这个时候因为遍历结束了,所以done的值就变成了true.
  • 第四次,因为函数已经return,next()返回的对象的value就是undefined,done值为true

yield的逻辑

理论上,yield提供了一种函数可以暂停的机制。而暂停的表达式就是yield。

  1. 当代吗执行到yield语句的时候,会暂停不会立即执行,并且把yield后面表达式的值当做返回对象的value属性值返回。
  2. 当下一次调用next()方法的时候,会从当前暂停的位置继续执行,知道遇到下一个yield或者return语句,返回其后面表达式的值。
  3. 如果都没有,那么一直执行到代码结束。这个时候返回对象value值是undefined。
  4. 当遍历结束的时候,done的值会从false变成true.

yield和return的区别?

两者相同点:都是返回跟在其后面的表达式的值。

两者不同点:yield有记忆功能,函数执行完以后会记录下来在从记录的位置继续执行;return并不具有记忆功能,从这返回以后函数不会在执行,仅仅执行一次。而Generator可以返回多个值,返回一系列的值。

function* fun(){
    console.log('执行了!')
}

var gen = fun();
setTimeout(function(){
    gen.next();
},3000)

这个console要到3s后才执行。

另外,yield语句只能在Generator函数里面。所以,yield也不能放到forEach函数里,也不能放到map函数里。

function funerr(){
    yield 123+321
}   //报错

再另外,如果yield语句在另外一个表达式里,必须在圆括号里。

function* fun(){
    console.log('hello'+yield)   // 语法错误
    console.log('hello'+(yield))   //正确
}

再再另外,yield作为函数参数或者赋值表达式的右边,可以不用加括号。

next方法的参数

yield本身并没有返回值,但是在next的参数里可以带一个参数,这个参数表示上一个yield语句的返回值。

//  next()的参数
function *f() {
  for(var i=0;true;i++){
    var reset = yield i;
    if(reset) {
      i = -1
    }
  }
}

var f = f()
console.log(f.next())       //{value:0,done:false}
console.log(f.next())       //{value:1,done:false}
console.log(f.next())       //{value:2,done:false}
console.log(f.next(true))   //{value:0,done:false} 

前边几次输出,这个时候reset的值是undefined,所以,i的值一次增加,当next函数传一个true,代表上一次的yield的值是true,那么这个时候i的值是-1,下一次循环从i等于-1开始.

Generator函数,从暂停状态到恢复运行,上下文是不变的。通过next方法的参数,就有办法在函数运行之后重新往函数里注入值,也就是在函数不同阶段注入不同的值,

function* foo(x) {
  var y = yield (x+2)
  var z = (yield ((y+3) *2))
  return x+y+z
}

var foo1 = foo(5)
console.log(foo1.next())     //{value:7,done:false}
console.log(foo1.next())     //{value:NaN,done:false}
console.log(foo1.next())     // {value:NaN,done:true}

var foo2 = foo(5) 
console.log(foo2.next())     // {value:7,done:false}   x:5 y:7
console.log(foo2.next(2))    //{value:10,done:false}   x:5 y:2
console.log(foo2.next(3))    // {value:10,done:true}   x:5 y:2 z:3

执行第二个next方法的时候,这个时候没有传入值,所以这个时候,y的值是undefined。所以。2 * undefined是NaN。

这个地方不是太好理解,可以再举一个例子

function* foo(x) {
  var y = 2 * (yield (x+2))    //之前说过的,yield作为表达式一定要在括号里
  var z =  (yield (y+3) *2)
  return x+y+z
}

var foo2 = foo(5)
log(foo2.next())    
log(foo2.next(2))  
log(foo2.next(3))  

用for...of代替next方法

每次总是调用next方法太过麻烦。for...of循环可以自动遍历Generator函数生成的Itearator对象,不需要调用next方法。

function* bar () {
  yield 3;
  yield 4;
  yield 1;
  yield 7;
  yield 9;
  return 0;
}

for (var i of bar()){
  console.log(i)    // 3,4,1,7,9
}

注意,上面的return语句并不在循环中,因为遍历到done为true的时候就会停止,所以,不会输出0.

理论上,实现了Iterator遍历器接口的扩展运算符(...),结构赋值,Array.from()内部调用的,都可以将Generator的返回值作为参数。比如:

function* foo(){
    yield:1;
    yield:3;
    return 9;
    yield:6
}

[...foo()]    // [1,3]
Array.from(foo())  // [1,2]
let [x,y] = foo()   //x=1 y=2

Generator.prototype.throw()

Generator返回的遍历器对象,都会有一个throw方法,这个方法挂在原型上。作用是在外部抛出在函数内部捕获的错误


function* gg() {
  try {
    yield ;
  } catch (e){
    console.log('内部错误',e)
  }
}

var gg = gg()
gg.next();

try {
  gg.throw('a')
  gg.throw('b')
} catch (e){
  console.log('外部错误',e)
}

在外部的try语句中,会连续抛出两个错误,第一个错误会被内部捕获,但是到了第二次的时候,因为内部的catch语句已经执行过了,所以就不会再次执行,所以错误会被外部的catch捕获。throw方法接受一个参数,可以再catch里输出,不过一般还是catch输出一个Error对象。


var g = function* () {
  try {
    yield 8;
  } catch (e) {
    console.log(e);
  }
};

var i = g();
console.log(i.next())
i.throw(new Error('出错了!'));
console.log(i.next(7))

如果在Generator函数内部没有部署try/catch,那么直接会在外部抛出错误,如果内外部都没有try/catch。那么函数直接错误中断执行。

如果函数内部抛出了错误,并不影响接下来yield或者return的执行。

Generator.prototype.return()

这个函数的作用就是return出一个值,并且终结Generator函数的执行。

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 }

next() throw() return()

这三个函数共同的作用是让Generator函数恢复执行。

next()就是让yield赋一个值,如果next()有参数,即是给yield传入一个值,如果没有参数,就是undefined。
throw()是把yield表达式替换成一个throw语句。
return()是把yield表达式替换成一个return语句。

理论上,三个函数都是在做同样一件事情。

yield* 表达式

如果在一个Generator函数里调用另外一个Generator函数,默认是没有效果的。

function* foo(){
    yield 'xx';
    yield 'yy';
}

function* bar(){
    yield 'aa';
    foo();
    yield 'bb';
}

for(var i of bar()){
    console.log(i)        // aa bb
}

如果想要在bar函数中执行foo函数,需要改写一下bar函数

function bar(){
    yield 'aa';
    yield* foo()
    yield 'bb'
}

输出的会是'aa' 'xx' 'yy' 'bb'

作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以简写成:

let obj = {
    * foo(){
        ....
    }
}

//等价于

let obj = {
    foo : function* (){
        ....
    }
}

Generator中的this

Generator函数返回一个遍历器对象,这个遍历器对象是Generator函数的实例,当然也就继承了函数原型上的那些方法。

function* foo(){
    yield 'xx'
}

let f = foo()
f00.prototype.hello = function(){
    console.log(123)
}

f instanceof foo // true
f.hello()   //123

所以,代码可以看出,f是foo的实例,同时可以调用foo原型上重写的方法。

但是:

function* foo(){
    this.a = 10
}

let f = foo()
f.a  //undefined

new foo()   //报错,foo is not a constructor

因为foo()返回的是一个遍历器对象,而不是this.
同时,Generator函数也不能和new一起使用。

那如何既能调用next()又能获取this呢。有一个变通的方法。用call绑定内部的this

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

let f = foo.call(F.prototype)

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

f.a  1
f.b  2
f.3  3

张小草1018
285 声望8 粉丝