「小记」ES6 之 Generator 函数

70 阅读7分钟

1. 简介

Generator 函数是 ES6 提供的一种异步编程的解决方案;

执行 Generator 函数会返回一个遍历器对象。即,Generator 函数除了状态机(封装了多个内部状态),还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator 函数内部的每一个状态;

形式上,Generator 函数是一个普通函数,有两个特征:

  • fucntion 关键字与函数名之间有一个星号 *
  • 函数内部使用 yield 表达式,定义不同的内部状态;
function* hellWorld(){
    yield 'hello'
    yield 'world'
    return 'ending'
}
var hw = helloWorld() // 函数被调用并不执行,返回一个指向内部状态的指针对象(Iterator Object)
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 函数是分段执行的,yield 表达式是暂停执行的标记,而 next 方法可以恢复执行;

// ES6 没有规定,function 关键字与函数名之间的星号位置,下面的写法都能通过:
fucntion*foo(x, y){...}
fucntion *foo(x, y){...}
fucntion* foo(x, y){...} // 推荐写法
fucntion * foo(x, y){...}

yield 表达式
遍历器对象的 next 方法的运行逻辑如下:

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

注意:

  • Generator 函数可以不用 yield 表达式,变成一个单纯的暂缓执行函数;
  • yield 表达式只能用在 Generator 函数里面,用在其他地方会报错;
  • 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
    
}
  • yield 表达式用作函数或放在赋值表达式的右边,可以不加括号;
function* demo(){
    foo(yield 'a', yield 'b') // OK
    let input = yield // OK
}

与 Iterator 接口的关系
任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象;

  • 由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口:
var myIterable = {}
// Generator 函数赋值给 Symbol.iterator 属性,从而使得 myIterable 对象具有了 Iterator 接口;
// 可以被 ... 运算符遍历;
myIterable[Symbol.iterator] = fucntion* (){
    yield 1
    yield 2
    yield 3
}
[...myIterator] // [1, 2, 3]
  • Generator 函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator 属性,执行后返回自身:
function* gen(){}
// gen 是一个 Generator 函数,调用它会生成一个遍历器对象 g
var g = gen() 
// g 的 Symbol.iterator 属性,也是一个遍历器生成对象,执行后返回 g 自己
g[Symbol.iterator]() === g // true

2. next 方法的参数

yield 表达式本身没有返回值,或者说总是返回 undefined 。 next 方法可以带一个参数,当作上一个 yield 表达式的返回值:

// 定义了一个可以无限运行的 Generator 函数 f
function* f(){
    for(var i=0; true; i++){
        var reset = yield i 
        // next方法没有传参,每次运行到 yield 表达式,变量 reset 的值总是 undefined ;
        // 当 next 方法传入参数 true ,变量 reset 就被重置为 true ;
        // 因此 i 会等于 -1 ,下个循环就会从-1 开始递增;
        if(reset){ i = -1 }
    }
}
var g = f()
g.next() // {value: 0, done: false}
g.next() // {value: 1, done: false}
g.next(true) // {value: 0, done: false}

Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过 next 方法的参数,就有办法在Generator 函数开始运行后,继续向函数体内部注入值。即,在Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为:

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

a.next() // {value: NaN, done: false}
// 第二次运行 next 方法时不带参数,y = 2*undefined(即 NaN)
// NaN/3 是 NaN ,返回对象的 value 属性是 NaN ;

a.next() // {value: NaN, done: true}
// 第三次运行 next 方法时不带参数,z = undefined
// x + y + z (5 + NaN + undefined = NaN)

var b = foo(5)
b.next() // {value: 6, done: false}
b.next(12) // {value: 8, done: false}
// 第二次运行 next 方法时传入参数 12 ,y = 2*12(即 24)
// 24/3 是 8 ,返回对象的 value 属性是 8 ;

b.next(13) // {value: 42, done: true}
// 第三次运行 next 方法时传入参数 13 ,z = 13
// x + y + z (5 + 24 + 13 = 42),返回对象的 value 属性是 42 ;

注意:由于 next 方法的参数表示上一个 yield 表达式的返回值,在第一次调用 next 方法时,传递的参数无效;

function* dataConsumer(){
    console.log('Started')
    console.log(`1. ${ yield }`)
    console.log(`2. ${ yield }`)
    return 'result'
}
let genObj = dataConsumer()
genObj.next() 
// Started
// {value: undefined, done: false}
genObj.next('a') 
// 1. a
// {value: undefined, done: false}
genObj.next('b') 
// 2. b
// {value: 'result', done: true}

3. 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

一旦 next 方法的返回对象的 done 属性为 true ,for...of 循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的 6,不包括在 for...of 循环之中。

实现斐波那契数列的🌰 :

// 斐波那契数列 0,1,1,2,3,5,8,13,21...
function* fbnq(){
    let [i, j] = [0, 1]
    for(;;){
        yield j
        [i, j] = [j, i+j]
    }
}
for(let n of fbnq()){
    if(n > 100) break;
    console.log(n)
}

通过 Generator 函数为原生js对象加上 Iterator 接口:

fucntion* objectEntries(obj){
    let propKeys = Reflect.ownKeys(obj)
    for(let key of propKeys){
        yield [key, obj[key]]
    }
}
let obj = {first: 'first', last: 'last'}

// === 第一种写法 ===
for(let [key, value] of objectEntries(obj)){
    console.log(`${key} : ${value}`)
}
// first : first
// last : last


// === 第二种写法 ===
// 将 Generator 函数加到对象的 Symbol.Iterator 属性上
obj[Symbol.Iterator] = objectEntries
for(let [key, value] of obj){
    console.log(`${key} : ${value}`)
}
// first : first
// last : last

除了 for...of 循环外,扩展运算符(...)、解构赋值和Array.form 方法内部调用,都是遍历器接口。它们都可以将 Generator 函数返回的 Iterator 对象,作为参数:

function* numbers(){
    yield 1
    yield 2
    return 3
    yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]

// Array.form 方法
Array.form(numbers()) // [1, 2]

// 解构赋值
let [x, y] = numbers()
x // 1
y // 2

7. yield* 表达式

如果在Generator 函数内部,调用另一个 Generator 函数,需要在前者的函数体内部,自己手动完成遍历:

fucntion* foo(){
    yield 'a'
    yield 'b'
}
function* bar(){
    yield 'x'
    for(let i of foo()){ // 手动遍历foo()
        console.log(i)
    }
    yield 'y'
}

for(let v of bar()){
    console.log(v)
}
// x
// a
// b
// y

ES6 提供 yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数:

fucntion* foo(){
    yield 'a'
    yield 'b'
}

function* bar(){
    yield 'x'
    yield* foo()
    yield 'y'
}
// 等同于
function* bar(){
    yield 'x'
    for(let v of foo()){
        yield v
    }
    yield 'y'
}
// 等同于
function* bar(){
    yield 'x'
    yield 'a'
    yield 'b'
    yield 'y'
}

for(let v of var()){
    console.loh(v)
}
// 'x'
// 'a'
// 'b'
// 'y'

使用 yield 表达式,返回一个遍历器对象;使用 yield* 表达式,返回遍历器对象的内部值:

function* inner(){
    yield 'hello'
}
function* outer1(){
    yield 'open'
    yield inner()
    yield 'close'
}
function* outer2(){
    yield 'open'
    yield* inner()
    yield 'close'
}
var gen1 = outer1()
gen1.next().value // 'open'
gen1.next().value // 返回一个遍历器对象
gen1.next().value // 'close'

var gen2 = outer2()
gen2.next().value // 'open'
gen2.next().value // 'hello'
gen2.next().value // 'close'

使用 yield* 表达式遍历完全二叉树:

// 下面是二叉树的构造函数
// 三个参数分别是左数、当前节点、右树
fucntion Tree(left, label, right){
    this.left = left
    this.label = label
    this.right = right
}
// 下面是中序(inorder)遍历函数
// 由于返回的是一个遍历器,所以用 Generator 函数
// 函数体内采用递归算法,所以左树和右树要用 yield* 遍历
function* inorder(t){
    if(t){
        yield* inorder(t.left)
        yield t.label
        yield* inorder(t.right)
    }
}
// 生成二叉树
function make(arr){
    // 判断是否为叶节点
    if(arr.length == 1) return new Tree(nill, arr[0],null)
    return new Tree(make(arr[0]), arr[1], make(arr[2]))
}

let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]])
var result = []
for(let node of inorder(tree)){
    result.push(node)
}
result // ['a','b','c','d','e','f','g']

参考链接: ECMAScript 6 入门