基本概念
Generator 函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
语法上:
形式上:
Generator 是个普通函数,但有两个特征: 一是: function 关键字与函数名之间有一个星号;二是: 函数体内部使用yield 表达式,定义不同的内部状态。
function* helloWorldGenerator() {yield 'hello'yield 'world'return 'ending'}var hw = helloWorldGenenrator()
该函数有三个状态: hello, world 和 return 语句。
// 调用hw.next() // {value: 'hello', done: false}hw.next() // {value: 'world', done: false}hw.next() // {value: 'ending', done: false}hw.next() // {value, undefined, done: true}
调用Generator函数,返回一个遍历器对象,代表Generator函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前内部状态的值,是yield 表达式后面那个表达式的值; done 属性是一个布尔值,表示是否结束遍历。
yield表达式
yield 表达式是暂停标志。
遍历器对象的next方法的运行逻辑:
- 遇到yield 表达式,就暂停执行后面的操作,并将yield 后的表达式的值,作为返回对象的value属性值。
- 下一次调用next方法时,再继续往下执行,直到遇到下一个yield 表达式。
- 如果没有遇到新的yield表达式,就一直运行,直到遇到return 语句,并将return 后的表达式的值,作为返回对象的vlaue属性值。
- 如果该函数没有return 语句,则返回的对象的value属性值为undefined.
yield 与 return
相似:
都能返回紧跟在语句后的表达式的值。
区别:
每次遇到yield,函数就暂停执行,下一次再从该位置继续往后执行。
一个函数执行多次yield 表达式,而只能执行一次return 语句。
yield表达式只能用在 Generator 函数里面,用在其它地方都会报错。
var arr = [1, [[2, 3], 4], [5, 6]]var flat = function* (a) {a.forEach(function(item) {if (typeof item !== 'number') {yield* flat(item)} else {yield item}})}for (var f of flat(arr)) {console.log(f)}
上面的代码会产生语法错误,因为 yield 表达式不能直接用在 forEach函数里。
var arr = [1, [[2, 3], 4], [5, 6]]var flat = function* (a) {var len = a.length;for (let i = 0; i < len; i++) {var item = a[i]if (typeof item !== 'number') {yield* flat(item)} else {yield item}}}for (var f of flat(arr)) {console.log(f)}// log123456
yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {console.log('Hello' + yield); // SyntaxErrorconsole.log('Hello' + yield 123); // SyntaxErrorconsole.log('Hello' + (yield)) // okconsole.log('Hello' + (yield 123)) // ok}
yield 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {foo(yield 'a', yield 'b'); // oklet input = yield; // ok}
与 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 属性,执行后返回自身。
next 方法的参数
yield 表达式本身没有返回值,或者说总是返回undefined。next 方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* f() {for (let i = 0; true; i++) {var reset = yield i;if (reset) { i = -1 }}}var 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方法带有一个参数是,变量reset 就被重置为这个参数。
Generator 函数从暂停状态到恢复执行,它的上下文状态是不变的。通过next 方法的参数,可以在Generator 函数开始运行后,继续向函数体内注入值。即可在Generator 函数运行的不同阶段,从外部向内部注入不同的值。
function* foo(x) {var y = 2 * (yield(x + 1))var z = yield(y / 3)return (x + y + z)}var a = foo(5)a.next() // {value: 6, done: false}a.next() // {value: NaN, done: false}a.next() // {value: NaN, done: false}var b = foo(5);b.next() // {value: 6, done: false}b.next(12) // {value: 8, done: false}b.next(13) // {vlaue: 42, done: true}
注意: 由于next 方法的参数表示上一个yield表达式的返回值,所以第一次使用next方法时,传递餐宿时无效的。V8引擎直接忽略第一次使用next方法的参数,只有从第二次使用next放开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
function* dataConsumer() {console.log('Started')console.log(`1, ${yield}`)conosle.log(`2, ${yield}`)return 'result'}let genObj = dataConsumer()genObj.next()// StartedgenObj.next('a')// 1, agenObj.next('b')// 2, b
如果要第一次调用next方法就能够输入值,可以在Generator函数外面再包一层。
function wrapper(generatorFunction) {return function(...args) {let generatorObject = generatorFunction(...args)generatorObject.next()return generatorObject;}}const wrapped = wrapper(function* () {conosle.log(`First input: ${yield}`)return 'DONE'})wrapped().next('hello!')// First input: hello!
for … of 循环
for…of 循环可以自动遍历Generator 函数运行时生成的Iterator 对象,且此时不需要调用next方法。
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
注意: 一旦next方法的返回对象done属性为true, for…of 循环就会中止,且不包含返回对象,所以return 语句返回的6,并不包含在 for…of 循环中。
function* fibonacci() {let [prev, curr] = [0, 1]for (;;) {yield curr;[prev, curr] = [curr, prev + curr]}}for (let n of fibonacci()) {if (n > 1000) breakconsole.log(n)}
function* objectEntries(obj) {let porpKeys = Reflect.ownKeys(obj);for (let propKey of propKeys) {yield [propKey, obj[propKey]]}}let jane = {first: 'Jane', last: 'Doe'}for (let [key, vlaue] 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 propkes) {yield [propKey, this[propKey]]}}let jane = {first: 'Jane', last: 'Doe'}jane[Symbol.iterator] = objectEntries;for(let [key, value] of jane) {console.log(`${key}: ${value}`)}// first: Jane// last: Doe
function* numbers() {yield 1yield 2return 3yield 4}// 扩展运算符[...numbers()] // [1, 2]// Array.from 方法Array.from(numbers()) // [1, 2]// 解构赋值let [x, y] = numbers()x // 1y // 2// for ... of 循环for (let n of numbers()) {console.log(n)}// 1// 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捕获。i 第二次抛出错误,由于Generator 函数内部的 catch 语句已经执行过了,不会再捕捉这个错误,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch 语句捕获。
throw 方法可接受一个参数,该参数会被catch 语句接受,建议抛出Error 对象的实例。
var g = function* () {try {yield} catch (e) {console.log(e)}}var i = g()i.next()i.throw(new Error('出错了'))// Error: 出错了
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}
如果 return 方法调用时,不提供参数,则返回值的value属性为 undefined
function* gen() {yield 1;yield 2;yield 3;}v 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;}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}
上例,调用return 方法后,就开始执行finally 代码块,然后就等到finally代码块执行完,再执行return 方法。
next(), throw(), return() 的共同点
它们的作用都是让 Geneartor 函数恢复执行,并且使用不同的语句替换yield 表达式。
next() 是将yield 表达式替换一个值
const g = function* (x, y) {let result = yield x + y;return result;}console gen = g(1, 2)gen.next() // {value: 3, done: false}gen.next(1) // {value: 1, done: true}// let result = yield x + y => let result = 1
第二个 next(1) 方法就相当于将yield 表达式替换成一个值 1; 如果next 方法没有参数,就相当于替换成undefined。
throw()是将yield 表达式替换成一个 throw语句
const g = function* (x, y) {let result = yield x + y;return result;};const gen = g(1, 2)gen.throw(new Error('出错了'))// let result = yield x + y; => let result = throw(new Error('出错了'))
return() 是将yield 表达式替换成一个return 语句
gen.return(2); // {value: 2, done: true}// let result = yield x + y; => let result = return 2;
yield* 表达式
默认情况下,不能直接在 Generator 函数内部调用另一个Generator函数。
但可以用 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';}
function* inner() {yield 'hello!'}function* outer() {yield 'open';yield inner();yield 'close'}var gen = outer1()gen.next().value // 'open'gen.next().value // 返回一个遍历器对象gen.next().value // 'close'function* outer2() {yeild 'open';yield* inner();yield 'close'}var gen = outer2()gen.next().value // 'open'gen.next().value // 'hello!'gen.next().value // 'close'
yield 后面的Generator 函数(没有return 语句时),不过是 for … of 的一种简写形式。
若是 return 语句时,则需要用 var value = yield iterator 的形式获取 return 语句的值。
function* concat(iter1, iter2) {yield* iter1;yield* iter2;}// 等同于function* concat(iter1, iter2) {for (var value of iter1) {yield value;}for (var value of iter2) {yield value}}
如果yield* 后面跟着一个数组,由于原生数组支持遍历器,因此会遍历数组成员.
function *gen() {yield* ['a', 'b', 'c'];}gen().next() // {value: 'a', done: false}
yield 命令后面如果不加星号,返回的整个数组,加了星号就表达返回的是数组的遍历器对象。
实际上,只要有Iterator 接口的数据结构,都可以被 yield* 遍历。
let read = (function* () {yield 'hello';yield* 'hello';})();read.next().value // 'hello'read.next().value // 'h'
如果被代理的 Generator 函数有 return 语句,那么可以向代理它的Generator 函数返回数据。
function* foo() {yield 2;yield 3;return 'foo';}function* bar() {yield 1;var v = yield* foo();console.log('v: ' + v)yield 4;}var it = bar();it.next() // {value: 1, done: false}it.next() // {value: 2, done: false}it.next() // {value: 3, done: false}it.next()// 'v: foo'// {value: 4, done: false}it.next(); // {value: undefined, done: true}
作为对象属性的 Generator 函数
let obj = {* myGeneratorMethod() {...}}// 等同于let obj = {myGeneratorMethod: function* () {}}
myGeneratorMethod 属性前面有一个星号,表示这个属性是一个 Generator 函数。
Generator 函数的this
Generator函数的遍历器是 Generator 函数的实例,也继承了 Generator 函数的 prototype 对象上的方法。
function* g() {}g.prototype.hello = function() {return 'hi'}let obj = g();obj instanceof g; // trueobj.hello() // 'hi'
g 返回的总是遍历器对象,而不是this 对象。
function* g() {this.a = 11}let obj = g()obj.next();obj.a // undefined
Generator 函数 g 在 this 对象上面添加了一个属性a, 但是obj 对象拿不到这个属性。
Generator 函数也不能直接跟 new 命令一起使用,否则会报错。
function* F() {yield this.x = 2;yield this.y = 3}new F(); // TypeError: F is not a constructor
因为 F 不是构造函数,所以不能 new 命令一起使用。
使用 this 的变通方法:
首先,生成一个空对象,使用 call 方法绑定 Generator 函数内部的this。
function* F() {this.a = 1;yield this.b = 2;yield this.c = 3;}var obj = {}var f = F.call(obj)f.next(); // {value: 2, done: false}f.next(); // {value: 3, done: false}f.next(); // {value: undefined, done: true}obj.a // 1obj.b // 2obj.c // 3
function* gen() {this.a = 1yield this.b = 2yield this.c = 3}function F() {return gen.call(gen.prototype)}var f = new F()f.next(); // {value: 2, done: false}f.next(); // {value: 3, done: false}f.next(); // {value: undefined, done: true}f.a // 1f.b // 2f.c // 3
Generator 与状态机
Generator 是实现状态机的最佳结构。
var ticking = true;var clock = function() {if (ticking) {console.log('Tick!')} else {console.log('Tock!')}ticking = !ticking}
clock 函数一共两个状态,运行一次就改变一次。这个函数用 Generator实现如下:
var clock = function* () {while (true) {console.log('Tick!')yield;console.log('Tock!')yield;}}
Generator 实现 更简洁、更安全、更符合函数式编程的思维,写法上也更优雅。
应用
Generator 可以暂停函数执行,返回任意表达式的值。
