# Generator 函数的语法
- 简介
- next 方法的参数
- for...of 循环
- Generator.prototype.throw()
- Generator.prototype.return()
- next()、throw()、return() 的共同点
- yield* 表达式
- 作为对象属性的 Generator 函数
- Generator 函数的this
- 含义
- 应用
- 异步应用
# 1. 简介
# (1) 基本概念
Generator 函数是 ES6 提供的一种异步编程解决方案。
语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator 函数特征:
function关键字与函数名之间有一个星号- 函数体内部使用
yield表达式,定义不同的内部状态
ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。所以下面的写法都能通过。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
2
3
4
由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function关键字后面。本书也采用这种写法。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
// 1. 调用 Generator 函数后,该函数并不执行,返回一个遍历器对象(`Iterator Object`)。
var hw = helloWorldGenerator(); // Object [Generator] {}
// 2. 下一步,必须调用遍历器对象的 next() 方法,使得指针移向下一个状态。
// 每次调用 next() 方法,内部指针就从函数头部或上一次停下来的地方开始执行,
// 直到遇到下一个 yield 表达式(或 return 语句)为止。
// 换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
console.log(hw.next()) // { value: 'hello', done: false }
console.log(hw.next()) // { value: 'world', done: false }
console.log(hw.next()) // { value: 'ending', done: true } // done: true 遍历完成
console.log(hw.next()) // { value: undefined, done: true } // 完成之后返回值都是一样
console.log(hw.next()) // { value: undefined, done: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
next() 返回的对象的value为 yield 表达式的值,done属性的boolean值表示遍历是否结束。
# (2) yield 表达式
遍历器对象的 next() 方法的运行逻辑如下。
(1)遇到yield,就暂停执行后面的操作,并返回 yield后表达式的值,作为对象的value属性值。
(2)下一次调用 next() 方法时,再继续往下执行,直到遇到下一个 yield 表达式。
(3)若不再有yield表达式则运行到函数结束,返回 return 表达式的值作为对象的value属性值。
(4)如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined 。
需要注意的是,yield 表达式后面的表达式,只有当调用 next 方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen() {
yield 123 + 456;
}
2
3
上面代码中,yield后面的表达式 123 + 456 ,不会立即求值,只会在 next() 方法将指针移到这一句时,才会求值。
# yield & return
- 都能返回紧跟在语句后面的那个表达式的值。
- 遇到
yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。 - 一个函数里面,只能执行一次
return语句,但是可以执行多次(或者说多个)yield表达式。 - 普通函数只能返回一个值(一次
return);Generator 函数可以返回多个值(多次yield)。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f(); // 函数 f 是一个 Generator 函数,只有调用 next() 方法时,才会执行。
setTimeout(function () {
generator.next()
}, 2000);
2
3
4
5
6
7
8
9
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); // yield 是用在普通回调函数中,而不是 generator 函数中
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
forEach()的回调函数是一个普通函数,在其中使用了yield表达式,报错。
可改用for循环。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
yield* flat(item); // yield 是用在 generator 函数中
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
}
2
3
4
5
6
7
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
yield虽然会将表达式的值抛出,但yield语句本身没有返回值,或者说总是返回undefined。
function* demo() {
foo(yield 'a', yield 'b'); // foo(undefined, undefined)
let input = yield; // input 值为 undefined
}
2
3
4
# (3) 与 Iterator 接口的关系
已经知道,任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
2
3
4
5
6
7
8
上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g; // true
2
3
4
5
6
7
上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。
# 2. next 方法的参数
因为yield语句本身值为undefined。next方法可以带一个参数,该参数就会被当作上一个yield语句的值。
function* f() {
for (var i = 0; true; i++) {
var reset = yield i; // 相当于 yield i; var reset = undefined;
if (reset) { i = -1; } // reset 值一直为 falsy,一直不会执行 if 中的语句
}
}
var g = f();
console.log(g.next()); // { value: 0, done: false }
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
// 传入 true 作为上一个 next() 中 yield 语句的值,相当于 reset = true,执行 if 中的语句
// 本次执行进入 if 语句, i = -1; 本次循环最后 i++; 再次碰到 yield i 相当于 yield 0
console.log(g.next(true)); // { value: 0, done: false }
console.log(g.next()); // { value: 1, done: false }
console.log(g.next()); // { value: 2, done: false }
console.log(g.next()); // { value: 3, done: false }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
通过next()方法的参数,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
另一个实例:
function* foo(x) {
var y = 2 * (yield (x + 1)); // yield (x + 1); var y = 2 * undefined;
var z = yield (y / 3); // yield (NaN / 3); var z = undefined;
return (x + y + z); // return (5 + NaN + undefined);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false } // var y = 2 * 12
b.next(13) // { value:42, done:true } // var z = 13
2
3
4
5
6
7
8
9
10
11
12
13
14
15
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
再看一个通过next方法的参数,向 Generator 函数内部输入值的例子。
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
2
3
4
5
6
7
8
9
10
11
12
13
14
上面代码是一个很直观的例子,每次通过next方法向 Generator 函数输入值,然后打印出来。
如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层。
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!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面代码中,Generator 函数如果不用wrapper先包一层,是无法第一次调用next方法,就输入参数的。
# 3. 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()) { // foo() 生成遍历器对象,for...of 直接遍历,相当于自动调用了 next()
console.log(v);
}
// 1 2 3 4 5
2
3
4
5
6
7
8
9
10
11
12
13
上面代码使用for...of循环,依次显示 5 个yield表达式的值。
注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
利用 Generator 函数和for...of循环,实现斐波那契数列的例子。
function* fibonacci() {
let [prev, curr] = [0, 1];
for (;;) {
yield curr;
[prev, curr] = [curr, prev + curr];
}
}
for (let n of fibonacci()) {
if (n > 20) break;
console.log(n);
}
// 1
// 1
// 2
// 3
// 5
// 8
// 13
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
利用for...of循环,可以写出遍历任意对象(object)的方法。
遍历对象 写法一:通过 Generator 函数
objectEntries()遍历对象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: Doe1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16遍历对象 写法二:将 Generator 函数加到对象的
Symbol.iterator属性上面。
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
除了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
// for...of 循环
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 4. Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个generatorObj.throw()方法(不是全局的throw命令),可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e); // 函数体内捕获函数体外抛出的错误
}
};
var i = g();
i.next();
try {
i.throw(new Error('错误一')); // 在 Generator 函数体外抛出错误
i.throw(new Error('错误二'));
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 Error: 错误一
// 外部捕获 Error: 错误二
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
throw() 方法可接受一个参数,该参数会被 catch 语句接收
上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。
全局
throw命令与g.throw方法是无关的,两者互不影响。generatorObj.throw()是对象中的方法,抛出的错误可以被generatorFunc()函数体内的catch捕获 全局的throw()方法,抛出的错误只能被Generator函数体外的catch语句捕获。 如果generatorFunc()函数内部没有部署try...catch代码块,那么generatorObj.throw()方法抛出的错误,将被外部try...catch代码块捕获。 如果generatorFunc()函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。 Generator 函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。
throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
}
var g = gen();
g.throw(1); // next() 一次也没执行过,Generator 函数还未开始执行。抛出的错只能抛到外部。
// Uncaught 1
2
3
4
5
6
7
8
9
10
11
上面代码中,g.throw(1)执行时,next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。
因为第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a
g.throw() // b,gen 函数体内的 catch 捕获错误之后,还会往后执行直到执行一次 yield 为止
g.next() // c
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。
function* foo() {
var x = yield 3;
var y = x.toUpperCase();
yield y;
}
var it = foo();
console.log(it.next()); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err); // TypeError: x.toUpperCase is not a function,x 是 number 类型没有此方法
}
console.log(it.next(99)); // { value: undefined, done: true },程序认为Generator函数已执行完毕
console.log(it.next(100)); // { value: undefined, done: true }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
上面代码中,第二个next方法向函数体内传入一个参数 42,数值是没有toUpperCase方法的,所以会抛出一个 TypeError 错误,被函数体外的catch捕获。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。
如果此后还调用next()方法,将返回 { value: undefined, done: true },即 JavaScript 引擎认为这个 Generator 已经运行结束了。
# 5. Generator.prototype.return()
遍历器对象 generatorObj 还有一个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 } // return 返回给定值的对象,并将 done 置为 true
// g.return() // { value: undefined, done: true } // 不提供参数时,value 为 undefined。
g.next() // { value: undefined, done: true } // gen() 函数已被 return 改为执行完毕
2
3
4
5
6
7
8
9
10
11
12
13
如果 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();
console.log(
g.next(), // { value: 1, done: false }
g.next(), // { value: 2, done: false }
g.return(7), // { value: 4, done: false } 调用`return`方法后,就开始执行`finally`代码块
g.next(), // { value: 5, done: false }
g.next(), // { value: 7, done: true } 等到`finally`代码块执行完,再执行`return`方法。
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 6. next()、throw()、return() 的共同点
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式本身。
next()是将yield表达式替换成一个值。const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.next(); // Object {value: 3, done: false} gen.next(1); // Object {value: 1, done: true} // 相当于将 let result = yield x + y // 替换成 let result = 1; // 如果next()方法没有参数,相当于 let result = undefined。1
2
3
4
5
6
7
8
9
10
11throw()是将yield表达式替换成一个throw语句。gen.throw(new Error('出错了')); // Uncaught Error: 出错了 // 相当于将 let result = yield x + y // 替换成 let result = throw(new Error('出错了'));1
2
3return()是将yield表达式替换成一个return语句。
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
2
3
# 7. yield* 表达式
需求:在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 调用另一个 Generator 函数 foo(), 需手动 for...of 遍历,如若嵌套更麻烦
for (let i of foo()) {
yield i;
}
yield 'y';
}
let genObj1 = bar();
console.log(
genObj1.next(), // { value: 'x', done: false }
genObj1.next(), // { value: 'a', done: false }
genObj1.next(), // { value: 'b', done: false }
genObj1.next(), // { value: 'y', done: false }
genObj1.next(), // { value: undefined, done: true }
)
// 使用 yield* 改写
function* bar2 () {
yield 'x';
// yield foo(); // 如果不加 * 号,实际上是将这个遍历器对象整体 yield 出去
yield* foo();
yield 'y';
}
let genObj2 = bar2()
console.log(
genObj2.next(), // { value: 'x', done: false }
genObj2.next(), // { value: 'a', done: false }
genObj2.next(), // { value: 'b', done: false }
genObj2.next(), // { value: 'y', done: false }
genObj2.next(), // { value: undefined, done: true }
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
yield* 后面跟的是一个遍历器对象,在外层 Generator 函数的遍历器对象中调用 .next() 时,程序会自动执行 yield* 后遍历器对象的 .next() 方法,并将结果作为外层结果 yield出去。
yield* 后面遍历器对象的 Generator 函数中没有return语句时,等同于在 Generator 函数内部,部署一个for...of循环。
function* concat(iterObj1, iterObj2) {
yield* iterObj1;
yield* iterObj2;
}
// 等同于
function* concat(iterObj1, iterObj2) {
for (var value of iterObj1) {
yield value;
}
for (var value of iterObj2) {
yield value;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yield* 后面遍历器对象的 Generator 函数中有return语句时,则需要用var value = yield* iterObj的形式获取return语句的值。
function* concat(iterObj1, iterObj2) {
let obj1Result = yield* iterObj1;
let obj2Result = yield* iterObj2;
}
2
3
4
实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。比如数组,字符串,类数组等。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
2
3
4
5
6
7
8
yield* 命令可以很方便地取出嵌套(多维)数组的所有成员。
function* iterTree(tree) {
if (Array.isArray(tree)) { // 如果参数是数组,则循环
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]); // 取出数组元素,则递归调用自己
}
} else {
yield tree; // 如果参数不是数组,则直接返回
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ]; // 多维数组
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
// ... 扩展运算符也默认调用 Iterator 接口(next())
console.log([...iterTree(tree)]) // ["a", "b", "c", "d", "e"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
使用yield*语句遍历完全二叉树:
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make(
[
[
['a'], 'b', ['c']
],
'd',
[
['e'], 'f', ['g']
]
]
);
console.log(tree)
/*
{
left:{
left: { left: null, label: 'a', right: null },
label: 'b',
right: { left: null, label: 'c', right: null }
},
label: 'd',
right:{
left: { left: null, label: 'e', right: null },
label: 'f',
right: { left: null, label: 'g', right: null }
}
}
*/
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
console.log(result) // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# 8. 作为对象属性的 Generator 函数
如果一个对象的某个属性是一个 Generator 函数,语法为:
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
// 简写:
let obj = {
* myGeneratorMethod () {
···
}
};
2
3
4
5
6
7
8
9
10
11
12
# 9. Generator 函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
2
3
4
5
6
7
8
9
10
上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
2
3
4
5
6
7
上面代码中,Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。
Generator 函数也不能跟new命令一起用,会报错。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this?
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj);
// 执行的是遍历器对象 f
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
// 生成的对象实例是 obj
obj.a // 1
obj.b // 2
obj.c // 3
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
将这两个对象统一:一个办法就是将obj换成F.prototype。这样属性和方法都挂在 prototype 上。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
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
2
3
4
5
6
7
8
9
10
11
12
13
14
再将F 改成构造函数,就可以对它执行 new 命令了。
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 10. 含义
略...
# 11. 应用
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
# (1) 异步操作的同步化表达
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
// 请求函数,回调中执行 next()
function request(url) {
makeAjaxCall(url, function(res){
it.next(res); // 响应数据需要通过 next() 的参数传给 yield 语句
});
}
// 主函数:调用请求函数发送请求
function* main() {
var result = yield request("http://some.url"); // 发送请求后暂停,在请求中调用 next() 继续
var resp = JSON.parse(result);
console.log(resp.value);
}
var it = main();
it.next(); // 开始执行主函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
通过 Generator 函数逐行读取文本文件。
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
2
3
4
5
6
7
8
9
10
上面代码打开文本文件,使用yield表达式可以手动逐行读取文件。
# (2) 控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
step1(function (value1) {
step2(value1, function(value2) {
step3(value2, function(value3) {
step4(value3, function(value4) {
// Do something with value4
});
});
});
});
// 采用 Promise 改写上面的代码。
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Generator 函数可以进一步改善代码运行流程。
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
scheduler(longRunningTask(initialValue));
function scheduler(task) {
var taskObj = task.next(task.value);
// 如果Generator函数未结束,就继续调用
if (!taskObj.done) {
task.value = taskObj.value
scheduler(task);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
注意,上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《异步操作》一章。
下面,利用for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法。
// 分布操作
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
// 先分工作,每一项工作再分布操作
let jobs = [job1, job2, job3];
function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield* iterateSteps(job.steps);
}
}
for (var step of iterateJobs(jobs)){
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// for...of的本质是一个while循环,所以上面的代码实质上执行的是下面的逻辑。
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
2
3
4
5
6
7
8
9
10
11
# (3) 部署 Iterator 接口
利用 Generator 函数,可以在任意对象上部署 Iterator 接口,实现对象的遍历。
function* iterEntries(obj) {
let keys = Object.keys(obj);
for (let i=0; i < keys.length; i++) {
let key = keys[i];
yield [key, obj[key]];
}
}
let myObj = { foo: 3, bar: 7 };
let genObj = iterEntries(myObj)
console.log(
genObj.next(), // { value: [ 'foo', 3 ], done: false }
genObj.next(), // { value: [ 'bar', 7 ], done: false }
genObj.next() // { value: undefined, done: true }
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
下面是一个对数组部署 Iterator 接口的例子,虽然数组原生就具有这个接口。
function* makeSimpleGenerator(array){
var nextIndex = 0;
while(nextIndex < array.length){
yield array[nextIndex++];
}
}
var gen = makeSimpleGenerator(['yo', 'ya']);
gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done // true
2
3
4
5
6
7
8
9
10
11
12
13
# (4) 作为数据结构
Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
2
3
4
5
上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}
2
3
实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
2
3
4
5
6
7
上面的函数,可以用一模一样的for...of循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。
# 12. 异步应用
# (1) 异步任务的封装
var axios = require('axios')
function* fn () {
console.log('start');
let res = yield axios.get('https://www.easy-mock.com/mock/5c36f2a48a4d935443d8598b/example/list')
console.log('接收到res', res)
}
let genObj = fn(); // 获得遍历器对象
let res = genObj.next(); // 开始执行,value 为拿到异步请求的处于 pending 状态的 promise 对象
console.log('out', res)
res.value.then((r) => { // promise 执行下一步 then
console.log(typeof r.data)
genObj.next(r.data) // 将请求的数据放入 next() 中作为上次执行 yield 语句的返回值
})
// start
// out { value: Promise { <pending> }, done: false }
// object
// 接收到res { data: { name: 'tom', age: 22 } }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# (2) Generator 自动执行控制
两种方法可以做到这一点。
- 回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
- Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
# 基于 Promise 对象的自动执行
// 首先,把fs模块的readFile方法包装成一个 Promise 对象。
var fs = require('fs');
var readFile = function (fileName){
return new Promise(function (resolve, reject){
fs.readFile(fileName, function(error, data){
if (error) return reject(error);
resolve(data);
});
});
};
var gen = function* (){
var f1 = yield readFile('/etc/fstab');
var f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
// 然后,手动执行上面的 Generator 函数:用then方法,层层添加回调函数
var g = gen();
g.next().value.then(function(data){
g.next(data).value.then(function(data){
g.next(data);
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
写出一个自动执行器:
function run(gen){
var g = gen();
// 只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。
function next(data){
var result = g.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
}
next();
}
run(gen); // 传入一个 Generator 函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15