Generator函数(上)
简介
基本概念
Generator函数
是Es6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
对于Generator函数
有多种理解角度。从语法上,首先可以把它理解成一个状态机,还是一个遍历器对象生成函数。返回遍历器对象,可以依次遍历Generator函数
内部的每个一状态。
形式上,Generator函数
是一个普通函数,但是有两个特征:一是function命令
与函数名
之间有一个星号;二是函数体内部使用yield语句
定义不同的内部状态(yield
在英语里是“产出”的意思)。
js
function* helloWorldGenerator () {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面的代码定义了一个Generator函数
——helloWorldGenerator
,它内部有两个yield语句
,即该函数返回3个状态:hello
、world
和return语句
(结束执行)。
Generator函数
的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数
后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象
,也就是遍历器对象
。
下一步,必须调用遍历器对象
的next方法
,使得指针移向下一个状态。也就是说,每次调用next方法
,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一条yield语句
(或return语句
)为止。换言之,Generator函数
是分段执行的,yield语句
是暂停执行的标记,而next方法
可以恢复执行。
js
hw.next() // {value: 'hello', done: false}
hw.next() // {value: 'world', done: false}
hw.next() // {value: 'ending', done: true}
hw.next() // {value: undefined, done: true}
上面的代码一共调用了4次next方法
。
第1次调用,Generator函数
开始执行,直到遇到第一条yield语句
为止。next方法
返回一个对象,它的value属性
就是当前yield语句
的值hello
,done属性
的值false
表示遍历还没有结束。
第2次调用,Generator函数
从上次yield语句
停下的地方,一直执行到下一条yield语句
。next方法
返回的对象的value属性
就是当前yield语句
的值world
,done属性
的值false
表示遍历还没有结束。
第3次调用,Generator函数
从上次yield语句
停下的地方,一直执行到return语句
(如果没有return语句
,就执行到函数结束)。next方法
返回的对象的value属性
就是紧跟在return语句
后面的表达式的值
(如果没有return语句
,则为undefined
),done属性
的值true
表示遍历已经结束。
第4次调用,此时Generator函数
已经运行完毕,next方法
返回的对象的value属性
为undefined
,done属性
为true
。以后再调用next方法
,返回的都是这个值。
总结一下,调用Generator函数
,返回一个遍历器对象,代表Generator函数
的内部指针。以后,每次调用遍历器对象的next方法
,就会返回一个有着value
和done
两个属性的对象。value属性
表示当前的内部状态的值,是yield语句
后面那个表达式的值;done属性
是一个布尔值,表示遍历是否已经结束。
ES6没有规定function关键字
与函数名
之间的星号
写在哪个位置。这导致下面的写法都能通过:
js
function * foo (x, y) { ... }
function *foo (x, y) { ... }
function* foo (x, y) { ... }
function*foo (x, y) { ... }
由于Generator函数
仍然是普通函数,所以一般的写法是上面的第3种,即星号
紧跟function关键字
后面。
yield语句
由于Generator函数
返回的遍历器对象只有调用next方法
才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数,yield语句
就是暂停标志。
遍历器对象的next方法
的运行逻辑如下:
-
遇到
yield语句
就暂停执行后面的操作,并将紧跟在yield
后的表达式的值作为返回对象的value属性
的值。 -
下一次调用
next方法
时再继续往下执行,直接遇到下一条yield语句
。 -
如果没有再遇到新的
yield语句
,就一直运行到函数结束,直接return语句
为止,并将return语句
后面的表达式的值作为返回对象的value属性
的值。 -
如果该函数没有
return语句
,则返回的对象的value属性
值为undefined
。
需要注意的是,yield语句
后面的表达式,只有当调用next方法
、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值(Lazy Evaluation)”的语法功能。
js
function* gen () {
yield 123 + 456;
}
上面的代码中,yield
后面的表达式123 + 456
不会立即求值,只有在next方法
将指针移到这一句时才求值。
yield语句
与return语句
既有相似之处,又有区别。相似之处在于都能返回紧跟在语句后的表达式的值。区别在于每次遇到yield
函数就会暂停执行,而return语句
不具备位置记忆的功能。一个函数里面只能执行一次(或者说一条)return语句
,但是可以执行多次(或者说多条)yield语句
。正常函数只能返回一个值,因为只能执行一次return语句
;Generator函数
可以返回一系列的值,因为可以有任意多条yield语句
。从另一个角度来看,也可以说Generator函数
生成了一系列的值,这就是其名称的来历(在英语中,“generator”这个词就是“生成器”的意思)。
Generator函数
可以不用yield语句
,这时就变成了一个单纯的暂缓执行函数。
js
function* f () {
console.log('执行了!');
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面的代码中,函数f如果是普通函数,在作为变量generator赋值时就会执行,但是函数f是一个Generator函数
,于是就变成只有调用next方法
时才会执行。
另外需要注意,yield语句不能用在普通函数中,否则会报错。
js
(function () {
yield 1;
})()
// SyntaxError: Unexpected number
上面的代码在一个普通函数中使用yield语句
,结果产生一个语法错误。下面是另外一个例子:
js
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* () {
a.forEach(function (item) {
if (typeof item !== 'number')
yield* flat(item);
else
yield item;
});
}
for (var f of flat(arr)) {
console.log(f)
}
上面的代码也会产生语法错误,因为forEach方法
的参数是一个普通函数,但是在里面使用了yield语句
。一种修改方法就是改用for循环
。
js
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* () {
for (var i = 0; i < arr.length; i++) {
var item = arr[i];
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
}
}
for (var f of flat(arr)) {
console.log(f)
}
// 1, 2, 3, 4, 5, 6
另外,yield语句
如果用在一个表达式中,必须放在圆括号里面。
js
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
yield语句
用作函数参数或用于赋值表达式的右边,可以不加括号。
js
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
与Iterator接口的关系
之前说过,任意一个对象的Symbol.iterator方法
等于该对象的遍历器对象生成函数,调用该函数就会返回该对象的一个遍历器对象。
遍历器对象本身也有Symbol.iterator方法
,执行后返回自身。
js
function* gen () {
// some code
}
var g = gen();
g[Symbol.iterator] === g // true
上面的代码中,gen
是一个Generator函数
,调用它会生成一个遍历器对象g
。它的Symbol.iterator属性
也是一个遍历器对象生成函数,执行后返回它自己。
next方法的参数
yield语句
本身没有返回值,或者说总是返回undefined
。next方法
可以带一个参数,该参数会被当作上一条yield语句
的返回值。
js
function* f () {
for (var 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}
// 设置参数之后,
// 在上一轮for循环中 reset 就为 true,所以i值被设置为了-1。
// 进入这一轮for循环时,i++语句使得i值变为0,所以会value的值为0
上面的代码先定义了一个可以无限运行的Generator函数f
,如果next方法
没有参数,每次运行到yield语句
,变量reset
的值总是undefined
。当next方法
带一个参数true
时,当前的变量reset
就被重置为这个参数,因而i
会等于-1
,下一轮循环就从-1
开始递增。
这个功能有很重要的语法意义。Generator函数
从暂停状态到恢复运行,其上下文状态
(context
)是不变的。通过next方法
的参数就有办法在Generator函数
开始运行后继续向函数体内部
注入值。也就是说,可以在Generator函数
运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
再看一个例子:
js
function* foo (x) {
var y = 2 * (yield (x + 1));
var z = 2 * (yield (y / 3));
return (x + y + z);
}
var a = foo(5);
console.log(a.next())
// Object{value: 6, done: false}
// 此时执行的是 yield x + 1 语句,所以value为6
console.log(a.next()) // Object{value: NaN, done: false}
// 由于 next() 没有参数
// 所以 上一条 yield (x + 1)语句的返回值为 undifined
// y = 2 * undefined,所以 y 为 NaN
// 此时 执行 yield (y / 3)语句,所以 valeu 为 NaN
console.log(a.next()) // Object{value: NaN, done: true}
// 同上 y 和 z 都为 NaN, 所以 x + y + z 的值为 NaN
var b = foo (5);
console.log(b.next())
// {value: 6, done: false}
// 此时执行的是 yield x + 1 语句,所以value为6
console.log(b.next(12))
// {value: 8, done: false}
// 由于 next(12) 有参数 12
// 所以 上一条 yield (x + 1)语句的返回值为 12
// y = 2 * 12,所以 y 为 24
// 此时 执行 yield (y / 3)语句,所以 value 为 8
console.log(b.next(13))
// {value: 55, done: true}
// 由于 next(13) 有参数 13
// 所以 上一条 yield(y / 3) 的返回值为 13
// z = 2 * 13,所以 z 为 26
// 此时 执行 x + y + z 语句,所以 value 为 5 + 24 + 26 = 55
// 注:书上的代码显示返回的是{value: 42, done: true}的计算结果是不小心忽略了 z = 2 * 返回值
注意,由于next方法
的参数表示上一条yield语句
的返回值,所以第一次使用next方法
时不能带有参数。V8引擎
直接忽略第一次使用next方法
时的参数,只有从第二次使用next方法
开始参数才是有效的。从语义上来讲,第一个next方法
是用来启动遍历器对象
的,所以不能带有参数。
如果想要第一次调用next方法
时就能够输入值,可以在Generator函数
外面再包一层。
js
function wrapper (generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args);
generatorObject.next();
return generatorObject;
}
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello') // First input: hello!
上面的代码中,Generator函数
如果不用wrapper
先包一层,是无法第一次调用next方法
就输入参数的。
再看一个通过next方法
的参数向Generator函数
内部输入值的例子:
js
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'
上面的代码是一个很直观的例子,每次通过next方法
向Generator函数
输入值,然后打印出来。
for…of循环
for...of循环
可以自动遍历Generator函数
,且此时不再需要调用next方法
。
js
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函数
和for...of循环
实现斐波那契数列
的例子:
js
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循环
时不需要使用next方法
。
前面介绍过,for...of循环
、扩展运算符(...)
、解构赋值
和Array.from方法
内部调用的都是遍历器接口
。这意味着,它们可以将Generator函数
返回的Iterator对象
作为参数。
js
function* numbers () {
yield 1;
yield 2;
return 3;
yield 4;
}
[...numbers()] // [1, 2]
Array.from(numbers()) // [1, 2]
let [x, y] = numbers()
x // 1
y // 2
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
利用for...of循环
可以写出遍历任意对象的方法。原生的JavaScript对象没有遍历接口,无法使用for...of循环
,通过Generator函数
为它加上这个接口就可以用了。
js
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
上面的代码中,对象jane原生不具备Iterator接口
,无法使用for...of
遍历。我们通过Generator函数
objectEntries为它加上遍历器接口,就可以用for...of循环
了。
加上遍历器接口的另一种写法是,将Generator函数
加到对象的Symbol.iterator属性
上。
js
function* objectEntries (obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[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
Generator.prototype.throw()
Generator函数
返回的遍历器对象
都有一个throw方法
,可以在函数体外抛出错误,然后在Generator函数
体内捕获。
js
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != 'a') throw e;
console.log('内部捕获', e);
}
}
}
var i = g();
i.next() // 启动遍历器
try {
i.throw('a')
// 在外部抛出异常 'a'
// 此时 g()内部的 e 的值为 'a'
// 同 e != 'a' 条件不成立,所以执行下面的console.log('内部捕获', e)语句
i.throw('b')
// 在外部抛出异常 'b'
// 此时 g()内部的 e 的值为 'b'
// 由于 'b' != 'a' 条件成立,所以函数体内执行 throw e 语句,所以会被当前的catch捕获,执行console.log('外面捕获', e)语句
} catch (e) {
console.log('外面捕获', e);
}
// 内部捕获 a
// 外部捕获 b
注意,不要混淆遍历器对象的throw方法
和全局的throw命令
。上面的错误是用遍历器对象的throw方法
抛出的,而不是用thorw命令
抛出的。后者只能被函数体外的catch语句
捕获。
看下面的例子:
js
var g = function* () {
while (true) {
try {
yield;
} catch (e) {
if (e != a) throw e;
console.log('内部捕获', a);
}
}
}
var i = g();
i.next();
try {
throw new Error('a'); // 不会进入到函数体内的 catch
throw new Error('b'); // 抛出异常后,此语句不会被执行
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 [Error: a]
如果Generator函数
内部没有部署try...catch代码块
,那么throw方法
抛出的错误将被外部try...catch代码块
捕获。
js
var g = function* () {
while (true) {
yield;
console.log('内部捕获', e)
}
}
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外面捕获', e);
}
// 外面捕获 a
上面的代码中,遍历器函数g内部没有部署try.catch代码块
,所以抛出的错误直接被外部catch
捕获。
如果Generator函数
内部部署了try...catch代码块
,那么遍历器的throw方法
抛出的错误不会影响下一次遍历。否则遍历会直接终止。
js
var gen = function* gen () {
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
g.throw();
} catch (e) {
g.next();
}
// hello
上面的代码只输出hello就结束了,因为第二次调用next方法
时遍历器状态已经变成终止了。
但如果使用throw命令
抛出错误,则不会影响遍历器状态。
js
var gen = function* gen () {
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
try {
throw new Error();
} catch (e) {
g.next();
}
// hello
// world
这种函数体内捕获错误的机制大大方便了对错误的处理。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数写一个错误处理语句。
js
foo('a', function (a) {
if (a.error) {
throw new Error(a.error);
}
foo('b', function (b) {
if (b.error) {
throw new Error(b.error);
}
foo('c', function (c) {
if (c.error) {
throw new Error(c.error);
}
console.log(a, b, c);
});
});
});
// 使用 Generator函数 可以大大简化上面的代码
function* g () {
try {
var a = yield foo('a');
var b = yield foo('b');
var c = yield foo('c');
} catch (e) {
console.log(e);
}
console.log(a, b, c);
}
反过来,Generator函数
内抛出的错误也可以被函数体外的catch
捕获。
js
function* g () {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log (generator) {
var v;
console.log('starting generator');
try {
v = generator.next(); // {value: undefined, done: true}
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v)
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
上面的代码一共运行了3次next方法
,在第2次运算的时候会抛出错误,然后第3次运行时,Generator函数
就已经结束,不再执行下去。
Generator.prototype.return()
Generator函数
返回的遍历器对象还有一个return方法
,可以返回给定的值,并终结Generator函数
的遍历。
js
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方法
的参数foo
。同时,Generator函数
的遍历终止,返回值的done属性
的值为true
,以后再调用next方法
,done属性
总是返回true
。
如果return方法
调用时不提供参数,则返回值的value属性
为undefined
。
如果Generator函数
内部有try...finally代码块
,那么return方法
会推迟到finally代码块
执行完再执行。
js
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: false}
以上,摘抄自阮一峰老师的《ES6标准入门》