ES6标准入门 学习笔记 (08)—— Generator篇

42 阅读7分钟

引入

系统学习ES6各种特性,了解背后的原理。

本章记录Generator函数

笔记

1. Generator简介

1.1 概述

Generator函数是ES6提供的一种异步编程解决方案,其语法行为与传统函数完全不同。

从语法上,首先可以把它理解为一个状态机,内部封装了多个状态。

此外,执行此函数会返回一个遍历器对象,所以还可以理解为一个遍历器对象生成函数,该对象遍历内部的每一个状态。

Generator函数有两个特征:

  1. function命令与函数名之间有一个星号
  2. 函数体内部使用yield(产出的意思)语句定义不同的内部状态

Generator函数的调用方法和普通函数一样,但是调用后,该函数并不会执行,返回的也不是运行结果,而是一个迭代器对象。

只有调用该迭代器对象的next方法,函数内部的指针就从函数头或上一次停下的地方开始执行,直到遇到下一条yield语句或return语句为止。

换言之,yield语句是暂停标志,next方法可以恢复执行。

function* foobar(){
    console.log(1)
    yield 'foo'
    console.log(2)
    yield 'bar'
    return 'foobar'
}

let foo = foobar()
foo.next() // 输出1 得到{value: 'foo', done: false}
foo.next() // 输出2 得到{value: 'bar', done: false}
foo.next() // 得到{value: 'foobar', done: true}

注意:若没有return语句时,会像普通函数一样补充一个return undefined

此外星号的位置要在function和函数名之间,左右是否间隔空格都不会影响

1.2 yield语句

yield语句是Generator函数内的暂停标志,Generator函数遇到yield语句就暂停执行后面的操作,并将紧跟在yield语句后的表达式的值作为返回的对象的value属性。

注意:

  1. yield语句后的表达式是惰性求值的,即运行到该yield语句才会进行求值
  2. yield语句与return语句的区别在于:yield语句可以执行多次,而return只有一次
  3. yield语句只能用于Generator函数中,其他地方都会报错
  4. yield表达式如果用在另一个表达式中,必须放在圆括号内,如let t = 'foo' + (yield)

补充:

yield语句本身并没有返回值(返回undefined)

所以let t = 'foo' + (yield 'bar')并不一定能得到'foobar',若next中没有附带参数,得到的将会是'fooundefined'

1.3 与Iterator接口的关系

由于Generator函数就是遍历器生成函数,因此可以把该其赋值给对象的Symbol.iterator属性,从而使得该对象具有Iterator接口。

let obj = {
    [Symbol.iterator]: function* (){
        yield 1;
        yield 2;
        yield 3;
        return 666 //return(done为true)的不会被取值
    }
}

console.log([...obj]) // [1, 2, 3]

Generator函数执行后的遍历器对象的Symbol.iterator属性就是该函数,执行后返回的是对象本身

2. next方法的参数

yield语句本身没有返回值或者说返回undefined

而next方法可以带有一个参数,该参数会被当做上一个yield语句的返回值

function* foo(x){
     let y = 2 * (yield (x+1))
     let z = yield (y/3)
     return (x+y+z)
}
let t = foo(5)
t.next() // {value: 6, done: false}  因为x+1=6

t.next(12) //{value: 8, done: false}
// 因为12会作为上一条yield语句的返回值,所以y=2*(12)=24
// 所以此处得到的是y/3=8

// let y = 2 * (yield (x+1)) 可以理解为函数运行到yield (x+1)就暂停了
// 直到运行下一个next(12)时,才把12作为yield (x+1)的值进行y的赋值运算

t.next(13) // {value: 42, done: true}
//13作为yield (y/3)的返回值,所以z = 13,之前得到y=24,所以return 5+24+13=42

注意:第一个next的参数会被忽略,所以第一个next仅仅用于启动遍历器对象。

若想要第一次调用next方法时就能输入值,可以在Generator函数外再包一层

function wrapper(func){ //包裹函数生成器
    return function (...args){
        let res = func(...args)
        res.next() // 此处执行了第一个next
        return res
    }
}

let wrappedFunc = wrapper( function* (){
    console.log('第一次next被执行')
    console.log(yield)
})

let t = wrappedFunc()
t.next('hello')

3.for of循环

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

function* foo(){
        yield 1;
        yield 2;
        yield 3;
        return 666 //return(done为true)的不会被取值
}

for (let val of foo()){
    console.log(val)
}
// 输出 1 2 3

但是注意:for of循环和扩展运算符一样,不会包含done为true的返回对象的值

此外,可以使用Generator函数为原生对象添加服务于for of遍历的Iterator接口

function* objectEntries(){
    let props = Object.keys(this) //注意要用this
    for (let prop of props){
        yield [prop, this[prop]] //返回pair的形式
    }
}

let foo = {a:'foo', b:'bar', c:'foobar'}
foo[Symbol.iterator] = objectEntries

for (let [k,v] of foo){
    console.log(`key:${k} val:${v}`)
}

4. Generator.prototype.throw()

Generator函数返回的遍历器对象带有throw方法,可以在函数体外抛出错误,然后在函数内使用try...catch捕获。若内部的错误被捕获,会自动执行一次next方法

let t = function* (){
    try{
        yield;
    }catch(e){
        console.log('内部捕获', e);
    }
    yield console.log('错误若被内部捕获则自动执行到下一条yield')
}();

t.next();
// t.throw('错误') // 但是更建议抛出错误实例
t.throw(new Error('错误'))

如果函数内部没有捕获错误,那么throw方法抛出的错误将被外部的try..catch捕获。此外,因为内部的错误没有在内部捕获,所以会中断内部的程序,导致done状态变为true。

let t = function* (){
    yield 1;
    yield 2;
    return 3;
}();

console.log(t.next()) // {value: 1, done: false}
try{
    t.throw('错误')
}catch(e){
    console.log('外部捕获',e);
}
console.log(t.next()) // {value: undefined, done: true} 因为内部错误未被捕获,程序已经被中断了

而外部抛出的错误并不会影响到迭代器的状态,可以继续使用next来恢复执行。

这种函数体内外错误捕获的机制方便了对错误的处理。而且出错的代码与处理错误的代码,实现了时间和空间上的分离。

5. Generator.prototype.return()

Generator函数返回的迭代器对象还有return方法,可以返回特定的值,并终结Generator函数的遍历。

但是注意:若Generator函数内有try..finally代码块时,return方法会被推迟到finally代码块执行完再执行

let g = function* (){
    yield 1;
    try{
        yield 2; //注意必须执行到代码块内,finally才有效
    }finally{
        yield 3;
    }
    yield 4;
}();

g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.return(6) // {value: 3, done: false}
g.next() // {value: 6, done: true} finally已结束,得到了之前return的6

6. yield*语句

如果在Generator函数内调用另一个Generator函数,默认情况下是没有效果的。

function* foo(){
    yield 1;
    yield 2;
}

function* bar(){
    foo() //在bar函数看来,得到的只是一个迭代器,并不会因此而暂停
    yield 3;
    yield 4;
}

for (let v of bar()){
    console.log(v)
}
// 3 4

使用yield*语句,可以实现在Generator函数内执行另一个Generator函数(也是一种递归思想)

function* foo(){
    yield 1;
    yield 2;
}

function* bar(){
    yield* foo() //yield*相当于将其右侧的迭代器拆解开,还原为一个个yield
    yield 3;
    yield 4;
}

// 等价于
function* bar2(){
    // for of循环yield
    for(let v of foo()){
        yield v;
    }
    yield 3;
    yield 4;
}

for (let v of bar()){
    console.log(v)
}
// 1 2 3 4

注意:若yield*后的Generator函数若需要获得其return值,那么需要用let val = yield* iter的形式

yield*的效果相当于将其右侧的迭代器拆解开,还原为一个个yield。

可以看做是for of循环使用yield的语法糖。

此外,yield*还可以用于任何实现了Iterator接口的对象。

let gen = function* (x){
   yield* x 
};

let t1 = gen([1,2,3])
let t2 = gen('abc')

for (let v of t1){
    console.log(v)
} // 1 2 3
[...t2] // ['a','b','c']

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

如果一个对象的属性是Generator函数,那么可以进行简写

let obj = {
    * gen1(){ //简写形式
        yield 1;
    },
    gen2: function* (){ //完整形式
        yield 2;
    }
}

8. Generator函数的this

Generator函数总是返回一个迭代器,ES6规定这个迭代器是该函数的实例,会继承函数prototype上的内容

但是Generator函数又不能简单的看做一个构造函数

  1. 不能使用new命令
  2. 返回的不是普通的this对象,而是迭代器对象

所以注意和普通函数的区别

9. 简单应用

异步操作的同步化表达

function* loadUI(){
    showLoading();
    yield loadUIAsync();
    hideLoading();
}

let loader = loadUI()
loader.next() //显示加载界面,并开始异步加载UI界面,加载完成后返回
loader.next() //隐藏加载界面

上述代码中loadUIAsync为异步函数,本来需要await、或者回调的方式来保证其执行顺序,但是使用yield可以以一种同步化的表达来编写代码,代码清晰

提供类似数组的结构

function* doStuff(){
    yield func1
    yield func2
    yield func3
}

for (task of doStuff()){
    task()
}