Generator函数语法解析

By admin in web前端 on 2019年5月7日

1.简介

    Generator函数是一个函数的内部状态的遍历器(也就是说,Generator函数是一个状态机)。
    形式上,Generator函数是一个普通函数,但是有两个特征。
  • function命令与函数名之间有一个星号*;
  • 函数体内部使用yield语句,定义遍历器的每个成员,即不同的内部状态。

    function* helloWorldGenerator() {
        yield 'hello';
        yield 'world';
        return 'ending';
    }
    let hw = helloWorldGenerator();
    

    console.log(hw.next());// { value: ‘hello’, done: false }

    console.log(hw.next());// { value: 'world', done: false }
    console.log(hw.next());// { value: 'ending', done: true }
    console.log(hw.next());// { value: undefined, done: true }
    

转载请注明出处:
Generator函数语法解析

  • Generator 函数的执行过程,其实是将同一个回调函数,反复传入 next
    方法的 value 属性。

  • Iterator 接口的 next
    方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行next方法,就必须同步地得到value和done这两个属性。
    如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。
    目前的解决方法是,Generator 函数里面的异步操作,返回一个 Thunk
    函数或者 Promise 对象,即value属性是一个 Thunk 函数或者 Promise
    对象,等待以后返回真正的值,而done属性则还是同步产生的。

总结:

  • 调用Generator函数,返回一个部署了Iterator接口的遍历器对象,用来操作内部指针。
  • 以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。
  • value属性表示当前的内部状态的值,是yield语句后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

Generator函数是ES6提供的一种异步编程解决方案,语法与传统函数完全不同。以下会介绍一下Generator函数。

Promise.resolve方法允许调用时不带参数,直接返回一个Resolved状态的Promise对象。
所以,如果希望得到一个Promise对象,比较方便的方法就是直接调用Promise.resolve方法。

2.next方法的参数

yield语句本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的返回值。

    function* f() {
        for(let i=0; true; i++) {
            var reset = yield i;
            if(reset) { i = -1; }
        }
    }
    let g = f();
    console.log(g.next()); // { value: 0, done: false }
    console.log(g.next()); // { value: 1, done: false }
    console.log(g.next(true));// { value: 0, done: false }

上面代码先定义了一个可以无限运行的Generator函数f,如果next方法没有参数,每次运行到yield语句,变量reset的值总是undefined。
 当next方法带一个参数true时,当前的变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

写下这篇文章的目的其实很简单,是想梳理一下自己对于Generator的理解,同时呢,为学习async函数做一下知识储备。

使用Generator函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

3.for…of循环

for...of循环可以自动遍历Generator函数,且此时不再需要调用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循环之中。
下面是一个利用generator函数和for...of循环,实现斐波那契数列的例子。

function* fibonacci() {
    let [prev, curr] = [0, 1];
    for (;;) {
        [prev, curr] = [curr, prev + curr];
        yield curr;
    }
}
for (let n of fibonacci()) {
    if (n > 1000) break;
    console.log(n);
}

从上面代码可见,使用for...of语句时不需要使用next方法。

Generator
函数,从语法上,可以把它理解成,是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator
函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历
Generator 函数内部的每一个状态。

4.throw方法

Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

    function* gen(x){
        try {
            var y = yield x + 2;
        } catch (e){
            console.log(e);
        }
        return y;
    }
    let g = gen(1);
    g.next();
    g.throw('出错了');// 出错了

上面代码的最后一行,Generator 函数体外,使用指针对象的 throw 方法抛出的错误,可以被函数体内的 try ... catch 代码块捕获。
这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

Generator函数

  1. 基本概念
  2. yield表达式
  3. next方法
  4. next方法的参数
  5. yield*表达式
  6. 与Iterator接口的关系
  7. for…of循环
  8. 作为对象属性的Generator函数
  9. Generator函数中的this
  10. 应用

每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。
Generator函数是分段执行的,yield语句是暂停执行的标记,而next方法可以恢复执行。
yield语句后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行。

5.yield*语句

如果yield命令后面跟的是一个遍历器,需要在yield命令后面加上星号,表明它返回的是一个遍历器。这被称为yield*语句。
其实yield关键字就是以一种更直观、便捷的方式让我们创建用于遍历有限序列集合的迭代器,而yield则用于将生成器函数的代码切片作为有限序列集合的元素(元素的类型为指令+数据,而不仅仅是数据而已)。下面我们一起看看yield关键字是怎样对代码切片的吧!
 定义生成器函数

function *enumerable(msg){
    document.write(msg)
    var msg1 = yield msg + '  after '
    document.write(msg1)
    var msg2 = yield msg1 + ' after'
    document.write(msg2 + ' over')
}
//上述代码最终会被解析为下面的代码:

var enumerable = function(msg){
    var state = -1
    return {
        next: function(val){
            switch(++state){
                case 0:
                    document.write(msg + ' after')
                    break
                case 1:
                    var msg1 = val
                    document.write(msg1 + ' after')
                    break
                case 2:
                    var msg2 = val
                    document.write(msg2 + ' over')
                    break
            }
        }
    }
}

基本概念

对于Generator函数(也可以叫做生成器函数)的理解,可以从四个方面:

形式上:Generator函数是一个普通的函数,不过相对于普通函数多出了两个特征。一是在function关键字和函数明之间多了’*’号;二是函数内部使用了yield表达式,用于定义Generator函数中的每个状态。

语法上:Generator函数封装了多个内部状态(通过yield表达式定义内部状态)。执行Generator函数时会返回一个遍历器对象(Iterator对象)。也就是说,Generator是遍历器对象生成函数,函数内部封装了多个状态。通过返回的Iterator对象,可以依次遍历(调用next方法)Generator函数的每个内部状态。

调用上:普通函数在调用之后会立即执行,而Generator函数调用之后不会立即执行,而是会返回遍历器对象(Iterator对象)。通过Iterator对象的next方法来遍历内部yield表达式定义的每一个状态。

写法上:*号放在哪里好像都可以也。看个人习惯吧,我喜欢第一种写法

function *gen () {}   √
function* gen () {}
function * gen () {}
function*gen () {}

一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象。

6.作为对象属性的Generator函数

如果一个对象的属性是Generator函数,可以简写成下面的形式。

    let obj1 = {
    * myGeneratorMethod() {

    }
};
//上面代码中,myGeneratorMethod属性前面有一个星号,表示这个属性是一个Generator函数。
//它的完整形式如下,与上面的写法是等价的。
    let obj2 = {
        myGeneratorMethod: function* () {

        }
    };

 此篇终,待续……

yield表达式

yield,英文意思即产生、退让的意思,因此yield表达式也有两种作用:定义内部状态暂停执行

举一个栗子吧: )

function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()   // Iterator对象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}

从上面代码中可以看出,gen函数使用yield表达式定义了两个内部状态。同时呢,也可以看出来,return语句只能有一个,而yield表达式却可以有多个。

执行gen函数之后,会返回一个遍历器对象,而不是立即执行gen函数。如果需要获取yield表达式定义的每个状态,需要调用next方法。

每调用一次next方法都会返回一个包含value和done属性的对象,此时会停留在某个yield表达式结尾处。value属性值即是yield表达式的值;done属性是布尔值,表示是否遍历完毕。

另外呢,yield表达式没有返回值,或者说返回值是undefined。待会会说明一下如何给yield表达式传递返回值。

需要注意的是,yield表达式的值,只有调用next方法时才能获取到。因此等于为JavaScript提供了手动的’惰性求值'(Lazy
Evaluation)的功能。

一般情况下,Generator函数会结合yield表达式使用,通过yield表达式定义多个内部状态。但是,如果不使用yield表达式的Generator函数就成为了一个单纯的暂缓执行函数,个人感觉没什么意义…

function *gen () {
  console.log('凯斯')
}

window.setTimeout(() => {
  gen().next()
}, 2000)

// 不使用yield表达式来暂停函数的执行,还不如使用普通函数呢..
// 所以Generator函数配合yield表达式使用效果更佳

另外,yield表达式如果用在另一个表达式中,需要为其加上圆括号。作为函数参数和语句是可以不使用圆括号。

function *gen () {
  console.log('hello' + yield) ×
  console.log('hello' + (yield)) √
  console.log('hello' + yield '凯斯') ×
  console.log('hello' + (yield '凯斯')) √
  foo(yield 1)  √
  const param = yield 2  √
}

调用Generator函数后,该函数并不执行,而是返回一个遍历器Iterator对象。
Generator 返回的是一个遍历器Iterator对象,所以可以使用 for..of ,
可以不用调用 next 方法。

next方法

yield表达式具有暂停执行的功能,而恢复执行的是next方法。每一次调用next方法,就会从函数头部或者上一次停下来的地方开始执行,直到遇到下一个yield表达式(return
语句)为止。同时,调用next方法时,会返回包含value和done属性的对象,value属性值可以为yield表达式、return语句后面的值或者undefined值,done属性表示遍历是否结束。

遍历器对象的next方法(从Generator函数继承而来)的运行逻辑如下

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield表达式后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到遇到return语句为止,并将return语句后面表达式的值,作为返回的对象的value属性值。
  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined。

从上面的运行逻辑可以看出,返回的对象的value属性值有三种结果:

  1. yield表达式后面的值
  2. return语句后面的值
  3. undefined

也就是说,如果有yield表达式,则value属性值就是yield表达式后面的指;如果没有yield表达式,value属性值就等于return语句后面的值;如果yield表达式和return语句都不存在的话,则value属性值就等于undefined。举个例子:
)

function *gen () {
  yield 1
  yield 2
  return 3
}

const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

根据next运行逻辑再针对这个例子,就很容易理解了。调用gen函数,返回遍历器对象。

第一次调用next方法时,在遇到第一个yield表达式时停止执行,value属性值为1,即yield表达式后面的值,done为false表示遍历没有结束;

第二次调用next方法时,从暂停的yield表达式后开始执行,直到遇到下一个yield表达式后暂停执行,value属性值为2,done为false;

第三次调用next方法时,从上一次暂停的yield表达式后开始执行,由于后面没有yield表达式了,所以遇到return语句时函数执行结束,value属性值为return语句后面的值,done属性值为true表示已经遍历完毕了。

第四次调用next方法时,value属性值就是undefined了,此时done属性为true表示遍历完毕。以后再调用next方法都会是这两个值。

function* helloWordGenerator() {
    yield "hello";
    yield "world";
    return "ending";
}

let g = helloWordGenerator();
let first = g.next();
console.info("first.value = " + first.value); // first.value = hello
console.info("first.done = " + first.done); // first.done = false
let second = g.next();
console.info("second.value = " + second.value); // second.value = world
console.info("second.done = " + second.done); // second.done = false
let third = g.next();
console.info("third.value = " + third.value); // third.value = ending
console.info("third.done = " + third.done); // third.done = true

next方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next()    // {value: [undefined, NaN], done: true}

从上面代码可以看出,第一次调用next方法时,value属性值是’hello
world’,第二次调用时,由于变量y的值依赖于变量x,而yield表达式没有返回值,所以返回了undefined给变量x,此时undefined
/ 2为NaN。

要解决上面的问题,可以给next方法传递参数。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function *gen () {
  var x = yield 'hello world'
  var y = x / 2
  return [x, y]
}
const g = gen()
g.next()    // {value: 'hello world', done: false}
g.next(10)    // {value: [10, 5], done: true}

当给第二个next方法传递参数10时,yield表达式的返回值为10,即var x = 10,所以此时变量y为5。

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

如果想要第一次调用next方法时就可以传递参数,可以使用闭包的方式。

// 实际上就是在闭包内部执行了一次next方法
function wrapper (gen) {
  return function (...args) {
    const genObj = gen(...args)
    genObj.next()
    return genObj
  }
}
const generator = wrapper(function *generator () {
  console.log(`hello ${yield}`)
  return 'done'
})
const a = generator().next('keith')
console.log(a)   // hello keith, done

实际上,yield表达式和next方法构成了双向信息传递。yield表达式可以向外传递value值,而next方法参数可以向内传递值。

Generator 函数是协程在 ES6
的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

yield*表达式 如果在Generator函数中调用另一个Generator函数,默认情况下是无效的。

function *foo () {
  yield 1
}
function *gen () {
  foo()
  yield 2
}
const g = gen()
g.next()  // {value: 2, done: false}
g.next()  // {value: undefined, done: true}

从上面代码中可以看出,并没有在yield 1处停止执行。此时就需要使用yield*
表达式。从语法角度上说,如果yield表达式后面跟着遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。实际上,yield*表达式是for…of循环的简写,完全可以使用for…of循环来代替yield*表达式

function *foo () {
  yield 1
}
function *gen () {
  yield* foo()
  yield 2
}
const g = gen()
g.next()   // {value: 1, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

// 相当于
function *gen () {
  yield 1
  yield 2
}

// 相当于
function *gen () {
  for (let item of foo()) {
    yield item
  }
  yield 2
}

如果直接使用了yield foo(),返回的对象的value属性值为一个遍历器对象。而不是Generator函数的内部状态。

function *foo () {
  yield 1
}
function *gen () {
  yield foo()
  yield 2
}
const g = gen()
g.next()   // {value: Generator, done: false}
g.next()   // {value: 2, done: false}
g.next()   // {value: undefined, done: true}

另外,任何数据类型(Array,
String)只要有Iterator接口,就能够被yield*遍历

const arr = ['a', 'b']
const str = 'keith'
function *gen () {
  yield arr
  yield* arr
  yield str
  yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'keith', done: false}
g.next() // {value: 'k', done: false}
...

如果在Generator函数中存在return语句,则需要使用let value = yield* iterator方式获取返回值。

function *foo () {
  yield 1
  return 2
}
function *gen () {
  var x = yield* foo()
  return x
}
const g = gen()
g.next()  // {value: 1, done: false}
g.next()  // {value: 2, done: true}

使用yield*表达式可以很方便的取出嵌套数组的成员。

// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
  console.log(+item)      // 1, 2, 3, 4
}

// 使用yield*表达式
function *gen (arr) {
  if (Array.isArray(arr)) {
    for (let i = 0; i < arr.length; i++) {
      yield * gen(arr[i])
    }
  } else {
    yield arr
  }
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
  console.log(item)       // 1, 2, 3, 4
}

异步操作需要暂停的地方,都用 yield 语句注明。

与Iterator接口的关系

任何一个对象的Symbol.iterator属性,指向默认的遍历器对象生成函数。而Generator函数也是遍历器对象生成函数,所以可以将Generator函数赋值给Symbol.iterator属性,这样就使对象具有了Iterator接口。默认情况下,对象是没有Iterator接口的。
具有Iterator接口的对象,就可以被扩展运算符(…),解构赋值,Array.from和web前端,for…of循环遍历了。

const person = {
  name: 'keith',
  height: 180
}
function *gen () {
  const arr = Object.keys(this)
  for (let item of arr) {
    yield [item, this[item]]
  }
}
person[Symbol.iterator] = gen
for (let [key, value] of person) {
  console.log(key, value)   // name keith , height 180
}

Generator函数函数执行之后,会返回遍历器对象。该对象本身也就有Symbol.iterator属性,执行后返回自身

function *gen () {}
const g = gen()
g[Symbol.iterator]() === g    // true

Generator
函数不同于普通函数,即执行它不会返回结果,返回的是遍历器对象(指针对象)。

for…of循环

for…of循环可以自动遍历Generator函数生成的Iterator对象,不用调用next方法。

function *gen () {
  yield 1
  yield 2
  yield 3
  return 4
}
for (let item of gen()) {
  console.log(item)  // 1 2 3
}

上面代码使用for…of循环,依次显示 3
个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。

调用 Generator 函数,会返回一个遍历器对象(指针对象)。

作为对象属性的Generator函数

如果一个对象有Generator函数,那么可以使用简写方式

let obj = {
  * gen () {}
}
// 也可以完整的写法
let obj = {
  gen: function *gen () {}
}

当然了,如果是在构造函数中,简写形式也是一样的。

class F {
  * gen () {}
}

调用指针 g 的 next
方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的 yield
语句。

Generator函数中的this

Generator函数中的this对象跟构造函数中的this对象有异曲同工之处。先来看看构造函数中的new关键字的工作原理。

function F () {
  this.a = 1
}
const f = new F()
  1. 调用构造函数F,返回实例对象f
  2. 将构造函数内部中的this指向这个实例对象
  3. 将构造函数中的原型对象赋值给实例对象的原型
  4. 执行构造函数中的代码

调用Generator函数会返回遍历器对象,而不是实例对象,因此无法获取到this指向的实例对象上的私有属性和方法。但是这个遍历器对象可以继承Generator函数的prototype原型对象上的属性和方法(公有属性和方法)。

function *Gen () {
  yield this.a = 1
}
Gen.prototype.say = function () {
  console.log('keith')
}
const g = new Gen()
g.a      // undefined
g.say()  // 'keith'

如果希望修复this指向性问题,可以使用call方法将函数执行时所在的作用域绑定到Generator.prototype原型对象上。这样做,会使私有属性和方法变成公有的了,因为都在原型对象上了。

function *Gen () {
  this.a = 1
  yield this.b = 2
  yield this.c = 3
}
const g = Gen.call(Gen.prototype)
g.next()   // {value: 2, done: false}
g.next()   // {value: 3, done: false}
g.next()   // {value: undefined, done: true}
g.a        // 1,继承自Gen.prototype
g.b        // 2,同上
g.c        // 3,同上

每次调用 next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield语句(或return语句)为止。

应用

Generator函数的应用主要在异步编程上,会在下一篇文章中分享。请期待噢: )

每次调用 next 方法,会返回一个对象,表示当前阶段的信息( value 属性和
done 属性)。value 属性是 yield
语句后面表达式的值,表示当前阶段的值;done
属性是一个布尔值,表示是否结束。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

网站地图xml地图
Copyright @ 2010-2019 澳门新葡亰官网app 版权所有