Generator 函数是一个状态机,封装了多个内部状态,执行 Generator 函数会返回一个遍历器对象,可以依次遍历 Generator 函数内部的每一个状态
Generator 的两个特征:
function* helloWorldGenerator () {
yield 'hello'
yield 'world'
return 'ending'
}
let hw = helloWorldGenerator()
上面代码定义了一个 Generator 函数 helloWorldGenerator,它内部有两个 yield 表达式(hello 和 world),即该函数有三个状态:hello,world 和 return 语句(结束执行)
Generator 函数与普通函数的不同之处在于,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,即遍历器对象(Iterator Object)
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数,yield 表达式就是暂停标志
遍历器对象的 next 方法的运行逻辑如下:
(1)遇到 yield 表达式,就暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值
(2)下一次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式
(3)如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值
(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
需要注意的是,yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能
function* gen () {
yield 123 + 456
}
上面代码中,yield 后面的表达式 123 + 456 不会立即求值,只会在 next 方法将指针移到这一句时,才会求值
Generator 函数中如果没有 yield 表达式,就变成了一个单纯的暂缓执行函数
function* f () {
console.log('执行了!')
}
let generator = f()
setTimeout(function () {
generator.next()
}, 2000)
// 2s 后打印“执行了!”
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 表达式本身没有返回值,或者说总是返回 undefined,next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值
function* f () {
for(let i = 0; true; i++) {
let reset = yield i
if (reset) { i = -1 }
}
}
let g = f()
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
上面代码先定义了一个可以无限运行的 Generator 函数 f,如果 next 方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined
当 next 方法带一个参数 true 时,变量 reset 就被重置为 true,因此 i 会等于 -1,下一轮循环就会从 -1 开始递增
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口
let myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
[...myIterable] // [1, 2, 3]
Generator 函数执行后,返回一个遍历器对象,该对象本身也具有 Symbol.iterator 属性,执行后返回自身,所以可以直接用 for...of 遍历
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 函数返回的遍历器对象,都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获
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
上面代码中,遍历器对象 i 连续抛出两个错误:
第一个错误被 Generator 函数体内的 catch 语句捕获;
第二次抛出错误,由于 Generator 函数内部的 catch 语句已经执行过了,不会再捕捉到这个错误,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获
let g = function* () {
while (true) {
try {
yield
} catch (e) {
if (e != 'a') throw e
console.log('内部捕获', e)
}
}
}
let i = g()
i.next()
try {
throw new Error('a')
throw new Error('b')
} catch (e) {
console.log('外部捕获', e)
}
// 外部捕获 [Error: a]
上面代码因为函数体外的 catch 语句块捕获了抛出的 a 错误,所以不会再继续执行 try 代码块里面剩余的语句
let g = function* () {
while (true) {
yield
console.log('内部捕获', e)
}
}
let i = g()
i.next()
try {
i.throw('a')
i.throw('b')
} catch (e) {
console.log('外部捕获', e)
}
// 外部捕获 a
上面代码中,Generator 函数 g 内部没有部署 try...catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获
throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next 方法
function* gen () {
try {
yield 1
} catch (e) {
console.log('内部捕获')
}
}
let g = gen()
g.throw(1)
// Uncaught 1
上面代码中,g.throw(1) 执行时,next 方法一次都没有执行过,这时抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错
throw 方法被捕获以后,会附带执行下一条 yield 表达式,也就是说,会附带执行一次 next 方法
let gen = function* gen (){
try {
yield console.log('a')
} catch (e) {
// ...
}
yield console.log('b')
yield console.log('c')
}
let g = gen()
g.next() // a
g.throw() // b
g.next() // c
上面代码中,g.throw 方法被捕获以后,自动执行了一次 next 方法,所以会打印 b
Generator 函数返回的遍历器对象,还有一个 return 方法,可以返回给定的值,并且终结遍历 Generator 函数
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 }
上面代码中,遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数 foo,并且,Generator 函数的遍历就终止,返回值的 done 属性为 true,以后再调用 next 方法,done 属性总是返回 true
如果return方法调用时,不提供参数,则返回值的value属性为 undefined
function* gen() {
yield 1
yield 2
yield 3
}
let g = gen()
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果 Generator 函数内部有 try...finally 代码块,且正在执行 try 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行
function* numbers () {
yield 1
try {
yield 2
yield 3
} finally {
yield 4
yield 5
}
yield 6
}
let g = numbers()
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
上面代码中,调用 return 方法后,就开始执行 finally 代码块,然后等到 finally 代码块执行完,再执行 return 方法
ES6 提供了 yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数
function* foo() {
yield 'a'
yield 'b'
}
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"
再来看一个对比的例子
function* inner() {
yield 'hello!'
}
function* outer1() {
yield 'open'
yield inner()
yield 'close'
}
let gen = outer1()
gen.next().value // "open"
gen.next().value // 返回一个遍历器对象
gen.next().value // "close"
function* outer2() {
yield 'open'
yield* inner()
yield 'close'
}
let gen = outer2()
gen.next().value // "open"
gen.next().value // "hello!"
gen.next().value // "close"
上面例子中,outer2 使用了yield*,outer1 没使用,结果就是,outer1 返回一个遍历器对象,outer2 返回该遍历器对象的内部值
本文转载自 http://es6.ruanyifeng.com/#docs/generator 并进行整理