/ ECMAScript6

ECMAScript6 之 Generator

标签: ECMAScript 6 javaScript

简介

Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

简单来说,一个 Generator 函数,就像是搭建一段楼梯,它规定了楼梯的步数以及怎么走,它生成的函数的执行就像是走楼梯一般,每走一步都会有具体的执行内容以及返回值,并且不能后台只能往前走( yield ),或是走到头了( return )。

一个 Generator 函数的标志就是 function 后面带 * 号。

一个简单的例子:

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

let hw = helloWorldGenerator()

这个 Generator 函数规定了 3 级台阶(两个 yield 和一个 return ),当函数调用的时候,搭建了一段楼梯,接下来就是往上走

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 函数生成的 hw (楼梯)一共可以走 3 步(调用 3next 方法),在 3next 方法调用结束之后,在调用这个函数,就返回固定的 { value: undefined, done: true }

返回值解释

如上代码所示, next 调用后的返回值中的 value 即为 yield 后跟的语句的值, done 代表楼梯是否走完,当 Generator 函数碰到 return 语句时,done 即为 true (有些同学可能会问要是没有返回值咋办?函数是有默认的返回值,即使不写,函数默认返回 undefined

yield

通过上述的解释,我们了解到,yield 其实规定了一步台阶,与 yield 息息相关的是 next 方法。接下来就说说 next 的机制:

  1. 调用 next 方法,函数就会走到第一个 yield 处,计算 yield 后的表达式,并返回
  2. 再次调用,函数接着执行,走到下一个 yield
  3. 接着调用,如果有 yield 重复第二步,否则走到 return 计算 return 后的表达式并返回
  4. 如果再次调用,直接返回 { value: undefined, done: true }

想一想 js 中函数的执行过程,在 Generator 函数出现之前,函数都是一股脑的从头执行到末尾,而 Generator 的出现让函数内部实现了一种暂停的效果,可以让函数一步一步的执行。

注意点

  1. 只能在 Generator 函数中使用
  2. 即使在 Generator 函数中使用了,yeild 也仅仅只能在函数的一级作用域中使用,比如不能再 forEach 中使用
  3. yeild 表达式如果在另一个表达式中使用,必须放在圆括号中

以下是上面 3 个注意点的错误例子

// eg: 1
(function (){
    // error
    yield 1
})()

// eg: 2
let arr = [1, [[2, 3], 4], [5, 6]]

let flat = function* (a) {
    // error
    a.forEach(function (item) {
        if (typeof item !== 'number') {
            yield* flat(item)
        } else {
            yield item
        }
    })
}

for (var f of flat(arr)){
    console.log(f)
}

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 函数与 Iterator 接口挂上关系。

熟悉熟悉 Iterator 的定义

任意一个对象的 Symbol.iterator 方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

而调用 Generator 函数,就能给我们返回一个遍历器,还记得 Symbol.iterator 方法的 next 吗?

所以在 Iterator 中我们说的内容,用 Generator 函数我们都可以实现,这里就不过多深入了。

next 方法的参数

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

注意是上一步的返回值,而在第一次调用 next 方法时,即使传入参数也毫无意义。

一个简单的例子

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

let a = foo(5)
a.next()
// { value: 6, done: false }
a.next()
// { value: NaN, done: false }
a.next()
// { value: NaN, done: true }

let b = foo(5)
b.next()
// { value: 6, done: false }
b.next(12)
// { value: 8, done: false }
b.next(13)
// { value: 42, done: true }

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

throw 方法可以认为是吧 yield 语句替换成一个 throw 语句。

let g = function* () {
    try {
        yield
    } catch (e) {
        console.log('内部捕获', e)
    }
}

let i = g()
i.next()

try {
    i.throw('a')
    i.throw('b')
} catch (e) {
    console.log('外部捕获', e)
}
// 内部捕获 a
// 外部捕获 b

当然如果 throw 对应的 yield 语句在 Generator 函数内部没有进行 try...catch 的话,是会向外部抛出的,就如例子中的 b

就像程序执行的那样,如果出错没有 try...catch 的话,是会往上抛的。

throw 方法,主要是为了在 Generator 函数外提供一个正常的错误提示机制。

Generator.prototype.return()

return 方法作用和 throw 差不多,只是它把 yield 语句换成了 return 语句。

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

let g = gen()

g.next()  
// { value: 1, done: false }
g.return('foo')
// { value: 'foo', done: true }
g.next()
// { value: undefined, done: true }

next throw return 共同点

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换 yield 表达式。

  • next() 是将 yield 表达式替换成一个值。
  • throw() 是将 yield 表达式替换成一个 throw 语句。
  • return() 是将 yield 表达式替换成一个 return 语句。

yield* 表达式

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

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

function* bar() {
    yield 'x'
    foo()
    yield 'y'
}

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

就如同例子中的一样,foo 中的楼梯( yield )并没有过加入到 bar 的楼梯上,而正常我们想要的效果应该是两个楼梯合并成一个楼梯,这时候,我们就需要使用到 yield* 表达式,如下所示

function* bar() {
    yield 'x'
    yield* foo()
    yield 'y'
}

// 等同于
function* bar() {
    yield 'x'
    yield 'a'
    yield 'b'
    yield 'y'
}

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

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

ok Generator 的内容就结束了。

注: 上面的例子大多数来自《ECMAScript 6入门》这本书。