基本概念
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)
}
// log
1
2
3
4
5
6
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 表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
function* demo() {
foo(yield 'a', yield 'b'); // ok
let 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()
// Started
genObj.next('a')
// 1, a
genObj.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) break
console.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 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
// 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; // true
obj.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 // 1
obj.b // 2
obj.c // 3
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(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // {value: undefined, done: true}
f.a // 1
f.b // 2
f.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 可以暂停函数执行,返回任意表达式的值。