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
步(调用 3
次 next
方法),在 3
次 next
方法调用结束之后,在调用这个函数,就返回固定的 { value: undefined, done: true }
。
返回值解释
如上代码所示, next
调用后的返回值中的 value
即为 yield
后跟的语句的值, done
代表楼梯是否走完,当 Generator
函数碰到 return
语句时,done
即为 true
(有些同学可能会问要是没有返回值咋办?函数是有默认的返回值,即使不写,函数默认返回 undefined
)
yield
通过上述的解释,我们了解到,yield
其实规定了一步台阶,与 yield
息息相关的是 next
方法。接下来就说说 next
的机制:
- 调用
next
方法,函数就会走到第一个yield
处,计算yield
后的表达式,并返回 - 再次调用,函数接着执行,走到下一个
yield
处 - 接着调用,如果有
yield
重复第二步,否则走到return
计算return
后的表达式并返回 - 如果再次调用,直接返回
{ value: undefined, done: true }
想一想 js
中函数的执行过程,在 Generator
函数出现之前,函数都是一股脑的从头执行到末尾,而 Generator
的出现让函数内部实现了一种暂停的效果,可以让函数一步一步的执行。
注意点
- 只能在
Generator
函数中使用 - 即使在
Generator
函数中使用了,yeild
也仅仅只能在函数的一级作用域中使用,比如不能再forEach
中使用 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
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个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入门》这本书。