ES6-标准入门·Generator 函数
Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。此前,只在 dva(内部封装 redux-saga)里使用过,此次深入了解之。
Generator 函数
简介
基本概念
Generator 函数可以理解成一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,该遍历器对象可以依次遍历 Generator 函数内部的每一个状态。换言之,Generator 函数除了是状态机,还是一个遍历器对象生成函数。
Generator 函数的调用方法与普通函数一样。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。
每次调用 next 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一条 yield 语句(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 语句是暂停执行的标记,而 next 方法可以恢复执行。
function* helloWorldGenerator() {
yield 'hello'
yield 'world'
return 'ending'
}
let hw = helloWorldGenerator()
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 方法的运行逻辑如下:
- 遇到 yield 语句就暂停执行后面的操作,并将紧跟在 yield 后的表达式的值作为返回对象的 value 属性值。
- 下一次调用 next 方法时再继续往下执行,直到遇到下一条 yield 语句。
- 如果没有再遇到新的 yield 语句,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值作为返回对象的 value 属性值。
- 如果该函数没有 return 语句,则返回对象的 value 属性值为 undefined。
只有调用 next 方法且内部指针指向该语句时才会执行 yield 语句后面的表达式,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
下面的代码中,yield 后面的表达式不会立即求值,只会在 next 方法将指针移到这一句时才求值。
function* gen() {
yield 123 + 456
}
Generator 函数可以不用 yield 语句,这时就变成了一个单纯的暂缓执行函数。下面的代码中,函数 f 如果是普通函数,在为变量 generator 赋值时就会执行。但是函数 f 是一个 Generator 函数,于是就变成只有调用 next 方法时才会执行。
function* f() {
console.log('执行了!')
}
let generator = f()
setTimeout(function() {
generator.next()
}, 2000)
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
console.log('Hello' + yield 123) // SyntaxError
console.log('Hello' + (yield 123)) // OK
}
与 Iterator 接口的关系
任意一个对象的 Symbol.iterator 方法等于该对象的遍历器对象生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。
let myIterable = {}
myIterable[Symbol.iterator] = function*() {
yield 1
yield 2
yield 3
}
;[...myIterable] // [1, 2, 3]
next 方法
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 函数从暂停状态到恢复运行,其上下文状态(context)是不变的。通过 next 方法的参数就有办法在 Generator 函数开始运行后继续向函数体内部注入值,从而调整函数行为。
function* foo(x) {
let y = 2 * (yield x + 1)
let z = yield y / 3
return x + y + z
}
let a = foo(5)
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{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 }
需要注意:由于 next 方法的参数表示上一条 yield 语句的返回值,所以第一次使用 next 方法时传递参数是无效的。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。从语义上讲,第一个 next 方法用来启动遍历器对象,所以不用带有参数。
function* dataConsumer() {
console.log('Started')
console.log(`1. ${yield}`)
console.log(`2. ${yield}`)
return 'result'
}
let genObj = dataConsumer()
genObj.next() // Started
genObj.next('a') // 1. a
genObj.next('b') // 2. b
for…of 循环
for…of 循环可以自动遍历 Generator 函数生成的 Iterator 对象,且此时不再需要调用 next 方法。
function* foo() {
yield 1
yield 2
return 3
}
for (let v of foo()) {
console.log(v) // 1 2
}
一旦 next 方法的返回对象的 done 属性为 true,for…of 循环就会终止,且不包含该返回对象,所以上面的 return 语句返回的 3 不包括在 for…of 循环中。
下面是一个利用 Generator 函数和 for…of 循环实现斐波那契数列的例子:
function* fibonacci() {
let [prev, curr] = [0, 1]
for (;;) {
;[prev, curr] = [curr, prev + curr]
yield curr
}
}
for (let n of fibonacci()) {
if (n > 1000) break
console.log(n)
}
利用 for…of 循环,给对象加上遍历器接口:
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj)
for (let propKey of propKeys) {
yield [propKey, obj[propKey]]
}
}
let jane = { first: 'Jane', last: 'Doe' }
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`)
}
// first: Jane
// last: Doe
另一种方法是直接将 Generator 函数添加到 Symbol.iterator 属性上:
function* objectEntries() {
let propKeys = Object.keys(this)
for (let propKey of propKeys) {
yield [propKey, this[propKey]]
}
}
jane[Symbol.iterator] = objectEntries
除了 for…of 循环,扩展运算符(…)、解构赋值和 Array.from 方法内部调用的都是遍历器接口。可以将 Generator 函数返回的 Iterator 对象作为参数:
function* numbers() {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
;[...numbers()] // [1, 2]
// Array.from方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers()
x // 1
y // 2
Generator.prototype.throw()
Generator 函数返回的遍历器对象都有一个 throw 方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function*() {
try {
yield
} catch (e) {
console.log('内部捕获', e)
}
}
var 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 语句捕获。
Generator.prototype.return()
Generator 函数返回的遍历器对象有一个 return 方法,可以返回给定的值,并终结 Generator 函数的遍历。
function* gen() {
yield 1
yield 2
yield 3
}
var 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 方法的参数。同时,Generator 函数的遍历终止,返回值的 done 属性为 true。如果 return 方法调用时不提供参数,则返回值的 vaule 属性为 undefined。
如果 Generator 函数内部有 try…finally 代码块,那么 return 方法会推迟到 finally 代码块执行完再执行。
function* numbers() {
yield 1
try {
yield 2
yield 3
} finally {
yield 4
yield 5
}
yield 6
}
var 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 }
yield* 表达式
如果在 Generator 函数内部调用另一个 Generator 函数,需要用到 yield*
语句。
function* foo() {
yield 'a'
yield 'b'
}
function* bar() {
yield 'x'
yield* foo()
yield 'y'
}
yield*
后面的 Generator 函数(没有 return 语句时)不过是 for…of 的一种简写形式,完全可以用后者替代。反之,在有 return 语句时则需要用 var value = yield* iterator
的形式获取 return 语句的值。
任何数据结构只要有 Iterator 接口,就可以被 yield*
遍历。下面的代码中,yield 命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。
function* gen() {
yield* ['a', 'b', 'c']
}
gen().next() // { value:"a", done:false }
Generator 函数 this
Generator 函数总是返回一个遍历器,这个遍历器是 Generator 函数的实例,它继承了 Generator 函数的 prototype 对象上的方法。
function* g() {}
g.prototype.hello = function() {
return 'hi!'
}
let obj = g()
obj instanceof g // true
obj.hello() // 'hi!'
但是,如果把 g 当作普通的构造函数,则并不会生效,因为 g 返回的总是遍历器对象,而不是 this 对象。
function* g() {
this.a = 11
}
let obj = g()
obj.a // undefined
Generator 函数也不能跟 new 命令一起用,否则会报错。
有一个变通方法可以让 Generator 函数返回一个正常的对象实例,既可以用 next 方法,又可以获得正常的 this:
function* gen() {
this.a = 1
yield (this.b = 2)
yield (this.c = 3)
}
function F() {
return gen.call(gen.prototype)
}
var f = new F()
f.next() // Object {value: 2, done: false}
f.next() // Object {value: 3, done: false}
f.next() // Object {value: undefined, done: true}
f.a // 1
f.b // 2
f.c // 3
上面代码中,使用 call 方法绑定 Generator 函数内部的 this 到原型 prototype 上,调用 next 方法完成 gen 内部所有代码的运行。这时,所有内部属性都绑定在原型对象上了,因此原型对象也就成了 gen 的实例。
金点网络-全网资源,一网打尽 » ES6-标准入门·Generator 函数
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。
- 是否提供免费更新服务?
- 持续更新,永久免费
- 是否经过安全检测?
- 安全无毒,放心食用