Generator函数的语法
简介
基本概念
Generator
函数是 ES6 提供的一种异步编程解决方案,从语法角度,他是一个状态机,封装了多个内部状态。执行后会返回一个遍历器对象,可以依次遍历 Generator
函数内部的每一个状态。也就是说他还是一个遍历器对象生成函数。
形式上,他是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号;二是,函数体内部使用 yield
表达式,定义不同的内部状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
调用 Generator
函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象。必须调用遍历器对象的 next
方法,使得指针指向下一个状态。
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
函数 helloWorldGenerator
有三个状态:hello
,world
和 return
语句(即函数结束)。调用 next
方法,指针会从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield
表达式或 return
语句为止。换言之,Generator
函数是分段执行的,yield
表达式是暂停执行的标记,而 next
方法可以恢复执行。直到遇到 return
语句或函数结束,才会结束执行。后续再调用 return
,也只会返回 { value: undefined, done: true }
。
ES6没有规定 *
号写在哪,以下几种方式都能通过:
function* foo() {}
function *foo() {}
function*foo() {}
function * foo() {}
yield表达式
由于 Generator
函数返回的遍历器对象,只有调用 next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
遍历器对象的 next
方法的运行逻辑如下:
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
⚠ 注意
yield
表达式后面的表达式,只有当调用 next
方法,内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
fucntion* add() {
yield 1 + 2 * 3
}
yield
后面的表达式不会立即计算,只有 next
方法指向针移到这句时才会求值
yield
和 return
既相似又不同。相似在于都能返回后面的值作 value
。不同在于遇到 yield
会暂停执行,下一次会从这里继续执行,一个函数可以执行多次 yield
;return
不具备记忆能力,一个函数只能执行一次 return
。
Generator
函数可以不加 yield
,这样他就只是一个单纯的暂缓执行函数。
function* fn() {
console.log('run')
}
var f = fn()
setTimeout(() => {
f.next()
}, 1000)
如果是普通函数,在为变量 f
赋值时就会立即执行,Generator
函数则会等到调用 f.next()
时才执行。
yield
表达式只能用在Generator
函数里面,它的外层必须是Generator
函数,用在其他地方都会报错。jsfunction* fn(arr) { arr.forEach(item = { yield item * 2 // 报错 }) }
上方代码
yield
外层是forEach
的回调函数,因此报错,解决方法是把forEach
改成for
,保证yield
外层是Generator
函数。yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。jsfunction* fn() { console.log('Hello' + yield) // 报错 console.log('Hello' + yield 123) // 不报错 console.log('Hello' + (yield)) // 不报错 console.log('Hello' + (yield 123)) // 不报错 }
与Iterator接口的关系
前面有提到过,任意一个对象的 Symbol.iterator
方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator
函数就是遍历器生成函数,因此可以把 Generator
赋值给对象的 Symbol.iterator
属性,从而使得该对象具有 Iterator
接口。
var myIterable = {}
myIterable[Symbol.iterator] = function* () {
yield 1
yield 2
yield 3
}
[...myIterable] // [1, 2, 3]
上面代码中,Generator
函数赋值给 Symbol.iterator
属性,从而使得 myIterable
对象具有了 Iterator
接口,可以被 ...
运算符遍历了。
Generator
函数执行后,返回一个遍历器对象。该对象本身也具有 Symbol.iterator
属性,执行后返回自身。
function* fn() {
yield 1
yield 2
yield 3
}
var f = fn()
f[Symbol.iterator]() === f // true
next方法的参数
yield
表达式本身没有返回值,或者说总是返回 undefined
。next
方法可以带一个参数,该参数会被当作上一个 yield
表达式的返回值。
function* fn() {
var f1 = yield 1
var f2 = yield 2
var f3 = yield 3
console.log(f1, f2, f3)
}
var f = fn()
f.next() // { value: 1, done: false }
f.next('a') // { value: 2, done: false } f1 = 'a'
f.next('b') // { value: 3, done: false } f2 = 'b'
f.next('c') // { value: undefined, done: true } f3 = 'c'
这使得 Generator
函数可以通过 next
方法的参数在开始运行后继续向函数内部注入值,从而调整函数行为。
⚠ 注意
由于 next
方法的参数表示上一条 yield 语句的返回值,所以第一次使用 next
方法时,传递参数是无效的。V8 引擎直接忽略第一次使用 next
方法时的参数,只有从第二次使用 next
方法开始,参数才是有效的。从语义上讲,第一个 next
方法用来启动遍历器对象,所以不用带有参数。
for...of循环
for...of
循环可以自动遍历 Generator
函数运行时生成的 Iterator
对象,且此时不再需要调用 next
方法。
function* fn() {
yield 1
yield 2
yield 3
return 4
}
for (var f of fn()) {
console.log(f)
}
上面代码使用 for...of
循环,依次显示 1、2、3。
⚠ 注意
for...of
会在 next
方法返回对象的 done
为 true
时停止循环,因此 return
语句的返回值不会遍历到。
JavaScript 原生对象没有遍历接口,可以通过 Generator
函数为它加上遍历器接口或者给对象的 Symbol.iterator
属性绑定 Generator
函数,这样就能使用 for...of
循环遍历对象了。
/**
* 添加遍历器接口
*/
function* objectEntries(obj) {
let propKeys = Object.keys(obj)
for (let propKey of propKeys) {
yield [propKey, obj[propKey]]
}
}
let obj = { first: 'hello', last: 'world' }
for (let [key, value] of objectEntries(obj)) {
console.log(`${key}: ${value}`)
}
/**
* Symbol.iterator 属性绑定 Generator 函数
*/
obj[Symbol.iterator] = function* () {
let propKeys = Object.keys(this)
for (let propKey of propKeys) {
yield [propKey, this[propKey]]
}
}
for (let [key, value] of obj) {
console.log(`${key}: ${value}`)
}
除此之外,扩展运算符 (...) 和 Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator
函数返回的 Iterator
对象,作为参数。
Generator.prototype.throw()
Generator
函数返回的遍历器对象都有一个 throw
方法,用于抛出错误,然后在 Generator
函数体内部 用 try...catch
捕获。throw
方法可以接受一个参数,该参数会被 catch
语句接收,建议抛出 Error
对象。
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
上面代码示例可以看出,第一次 throw
抛出错误被 Generator
函数内部的 try...catch
捕获。第二次 throw
抛出错误,由于内部的 catch
语句已经执行过了,无法再捕捉这个错误了,因此被抛出 Generator
函数体,在外部 try...catch
捕获。
如果内部没有 try...catch
语句,那么这个错误将直接抛到 Generator
函数体外部。此时函数会因为遍历报错导致直接终止遍历。
function* fn() {
yield 1
yield 2
yield 3
}
var g = fn()
g.next() // 1
g.throw('a') // Uncaught a 循环终止
⚠ 注意
不要混淆 throw
方法与函数体内部的 throw
命令语句,后者只能被函数体内部的 catch
语句捕获。
throw
方法在被 捕获 后会附带执行下一条的 yield
语句,即附带执行一次 next
方法。
这种函数体内捕获错误的机制大大方便了对错误的处理。对于多个 yield
表达式,可以 只用 try...catch
代码块来捕获错误。如果使用回调函数的写法想要捕获多个错误, 就不得不每个函数写←个错误处理语句,而现在只在 Generator
函数内部写 catch
语句 就可以了。
Generator.prototype.return()
Generator
函数返回的遍历器对象,还有一个 return
方法,可以返回给定的值,并且终结遍历 Generator
函数。
function* fn() {
yield 1
yield 2
yield 3
}
var g = fn()
g.next() // 1
g.return('foo') // { value: 'foo', done: true }
g.next() // { value: undefined, done: true }
如果 Generator
函数内部有 try...finally
代码块,那么 return
方法会推迟到 finally
代码块执行完再执行。
function* fn() {
try {
yield 1
}
finally {
yield 2
yield 3
}
}
var g = fn()
g.next() // { value: 1, done: false }
g.return('foo') // { value: unde2fined, done: false }
g.next() // { value: 3, done: false }
g.next() // { value: 'foo', done: true }
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*
表达式,foo()
就会作为 Generator
函数执行。
function* bar() {
yield 'x'
yield* foo()
/**
* 等同于:
* for (let v of foo()) {
* yield v
* }
*/
yield 'y'
}
for (let v of bar()) {
console.log(v)
}
// 'x'
// 'a'
// 'b'
// 'y'
从语法角度看,如果 yield
命令后面跟的是一个遍历器对象,那么需要在 yield
命令后面加 * 星号,表明它返回的是一个遍历器。这样,yield*
表达式就会调用遍历器接口,也就是调用 Generator
函数。这被称为 yield*
语句。
yield*
表达式可以很方便地遍历所有有 Iterator
接口的数据。。
function* fn() {
yield 'hello'
yield* 'hello'
yield* [1, 2, 3]
}
// 'hello'
// 'h'
// 'e'
// 'l'
// 'l'
// 'o'
// 1
// 2
// 3
如果代理的 Generator
函数有 return
语句,那么 return
语句的返回值,就作为 yield*
表达式的返回值。
function* fn() {
yield 2
yield 3
return 'foo'
}
function* bar() {
yield 1
var v = yield* fn()
console.log('v: ' + v)
yield 4
}
let b = bar()
b.next() // { value: 1, done: false }
b.next() // { value: 2, done: false }
b.next() // { value: 3, done: false }
b.next() // v: foo { value: 4, done: false }
b.next() // { value: undefined, done: true }
作为对象属性的Generator函数
如果对象的方法是 Generator
函数,可以将该对象的方法名放在方括号内。
let obj = {
* myGeneratorMethod() {
// ···
}
}
/**
* 等价于以下写法
*/
let obj = {
myGeneratorMethod: function* () {
// ···
}
}
Generator函数的this
Generator
函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator
函数的实例,也继承了 Generator
函数的 prototype
对象上的方法。Generator
函数的 this
是不继承的。
function* g() {}
g.prototype.hello = function () {
return 'hi!'
}
let obj = g()
obj instanceof g // true
obj.hello() // 'hi!'
上面代码表明,Generator
函数 g
返回的遍历器 obj
,是 g
的实例,继承了 g.prototype
。但是,如果把 Generator
函数当作普通的构造函数,并不会生效,因为 Generator
函数返回的总是遍历器对象,而不是 this
对象。
function* g() {
this.a = 11
}
let obj = g()
obj.a // undefined
上面代码中,Generator
函数 g
在 this
对象上添加了一个属性 a
,但是 obj
对象拿不到这个属性。
含义
总结
Generator
函数是 ES6 引入的一种异步编程解决方案,它允许函数在执行过程中被暂停和恢复。这种函数在形式上是普通函数,但有两个特征:一是在 function
关键字和函数名之间有一个星号(*),二是函数体内部使用 yield
表达式来定义不同的内部状态。
Generator
函数执行后返回一个遍历器对象,可以通过调用该对象的 next
方法来遍历函数内部的每一个状态。yield
表达式用于暂停函数执行,直到下一次调用 next
方法时才继续执行。此外,yield
表达式后面的表达式只有在调用 next
方法,内部指针指向该语句时才会执行,这为 JavaScript 提供了手动的 “惰性求值” 功能。
Generator
函数与 Iterator
接口紧密相关,可以将 Generator
函数赋值给对象的Symbol.iterator
属性,使其具有 Iterator
接口。这样,对象就可以被 for...of
循环遍历,或者使用扩展运算符(...)和 Array.from
方法。
next
方法可以带一个参数,该参数会被当作上一个 yield
表达式的返回值,这使得 Generator
函数可以在开始运行后继续向函数内部注入值,从而调整函数行为。for...of
循环可以自动遍历 Generator
函数运行时生成的 Iterator
对象,且此时不再需要调用 next
方法。
Generator
函数还提供了 throw
和 return
方法。throw
方法用于抛出错误,并在 Generator
函数体内部用 try...catch
捕获。return
方法可以返回给定的值,并终结遍历 Generator
函数。如果 Generator
函数内部有 try...finally
代码块,那么 return
方法会推迟到 finally
代码块执行完再执行。
yield*
表达式允许在一个 Generator
函数内部调用另一个 Generator
函数,或者遍历一个具有 Iterator
接口的数据结构。这使得代码更加简洁,可以方便地遍历所有有 Iterator
接口的数据。