Generator,生成器函数,ES6中最难理解的语法,没有之一。Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  • 形式上,Generator 函数是一个普通函数,但是有两个特征 :
    • function关键字与函数名之间有一个星号*
    • 函数体内部使用yield表达式,定义不同的内部状态yield在英语里的意思就是“产出”)。

语法

只要给一个函数关键字后面添加一个星号*,那么这个函数就被称之为生成器generator函数

调用

调用生成器函数,不会立即执行函数体,而是会返回一个Iterator迭代器对象,调用next() 方法这继续往后执行,碰到yield关键字就暂停

next只是给yield下指令,有了next就执行一个yieldyield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的 “惰性求值” (Lazy Evaluation)的语法功能。

  • value:表示每次执行时yield后面的内容(每次调用都会从上一次停止的地方继续运行
  • done:表示当前函数状态,是否执行完
1
2
3
4
5
6
7
8
9
10
11
12
13
function* generator (){
yield 'a'
yield 'b'
yield 'c'
return 'ending'
}

let g = generator() // 并不是执行,相当于new了一个实例对象,这就是*作用
console.log(g.next()); // { value: 'a', done: false }
console.log(g.next()); // { value: 'b', done: false }
console.log(g.next()); // { value: 'c', done: false }
console.log(g.next()); // { value: 'ending', done: true }
console.log(g.next()); // { value: undefined, done: true }
1
2
3
4
5
6
7
8
9
10
function* foo () {
var o = 1
yield o++
yield o++
yield o++
}
let gen = foo()
console.log(gen.next()); // { value: 1, done: false }
let gener = foo()
console.log(gener.next()); // { value: 1, done: false }

yield没有返回值:undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* g() {
let a = 1
let b = yield a++
console.log(b);
let c = yield a++
}

let gen = g()
console.log(gen.next());
console.log(gen.next());
// 输出如下:
// { value: 1, done: false } a
// undefined b
// { value: 2, done: false } a


调用的是异步代码(先执行同步代码再执行异步代码):

  1. 分步调用next() :
    由运行结果可知:前四次调用中,返回 {value: Promise, done: false} 是同步的先执行,而setTimeout中的代码(打印)是异步,等同步代码执行完后再执行。而第五次调用时,返回 {value: undefined, done: false} 和调用B() 打印b都是同步代码,故会按顺序执行
1
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
function A(num) {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log(`异步的a${num}`)
resolve()
},0)
})
}
function B() {
console.log('同步的b')
}
function* gen() {
yield A(1)
yield A(2)
yield A(3)
yield A(4)
yield B()
}
let g = gen()
g.next() // 第一次调用
// {value: Promise, done: false} 异步的a1
g.next() // 第二次调用
// {value: Promise, done: false} 异步的a2
g.next() // 第三次调用
// {value: Promise, done: false} 异步的a3
g.next() // 第四次调用
// {value: Promise, done: false} 异步的a4
g.next() // 第五次调用
// 同步的b {value: undefined, done: false}
g.next() // 第六次调用
// {value: undefined, done: true}
  1. 一次性调用next() :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function A(num) {
return new Promise((resolve, reject)=>{
setTimeout(()=>{
console.log(`异步的a${num}`)
resolve()
},0)
})
}
function B() {
console.log('同步的b')
}
function* gen() {
yield A(1)
yield A(2)
yield A(3)
yield A(4)
yield B()
}
let g = gen()
g.next()
g.next()
g.next()
g.next()
g.next()

执行结果:

分析:前四个yield表达式后面都是setTimeout异步代码,会按顺序加入任务队列;执行到第5个yield ,后面是同步代码,会打印出同步的b ,之后返回 {value: undefined, done: false} ,同步任务完成后再从任务队列中依次执行刚刚的异步任务。 注意:如果一次性调用的是6次g.next() ,则打印完同步的b后返回的是 {value: undefined, done: true}

遍历

因为执行 Generator 函数会返回一个遍历器对象,所以可以对其进行遍历操作:返回所有的value ,最后return的值不会被返回,因为for of遇到donetrue就停止遍历了。

for-of 循环只会遍历迭代器 done 的值为 false 的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function* generator (){
yield 'a'
yield 'b'
yield 'c'
return 'ending'
}

for(let item of generator()){
console.log(item)
}
// 输出:
// a
// b
// c

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

1
2
3
4
5
6
7
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]

Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性执行后返回自身

1
2
3
4
function* gen(){
// some code}
var g = gen();
g[Symbol.iterator]() === g // true

next()方法传参

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值,也就是替换掉上一个整个yield表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function* g() {
let a = 1
let b = yield a++
console.log(b);
let c = yield a++
console.log(a);
console.log(c);
}

let gen = g()
console.log(gen.next());
console.log(gen.next(3)); // next的参数,用于指定被我触发的yield的执行结果
console.log(gen.next(2));
// 输出如下:
// { value: 1, done: false }
// 3
// { value: 2, done: false }
// 3
// 2
// { value: undefined, done: true }

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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语句捕获。
  • 第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例

不要混淆遍历器对象的throw方法和全局的throw命令。throw命令与g.throw方法是无关的,两者互不影响。 上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。全局的throw命令只能被函数体外的catch语句捕获

1
2
3
4
5
6
7
8
9
function* gen() {
try {
yield 1;
} catch (e) {
console.log('内部捕获');
}
}
var g = gen();
g.throw(1);// Uncaught 1

上面代码中,g.throw(1) 执行时, next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。

这种行为其实很好理解,因为第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。


一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了 。如果此后还调用next方法,将 返回一个value属性等于undefined、done属性等于true的对象 ,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

如下面的例子:

1
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
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();
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

上面代码一共三次运行next方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了

Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数

1
2
3
4
5
6
7
8
9
10
11
12
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代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块, finally执行完以后,再次调用next() 方法会刚刚return的参数,如果没传参就返回undefined ,至此整个函数才会结束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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代码块,不执行try里面剩下的代码了,
// 然后等到finally代码块执行完,再返回return()方法指定的返回值。

next()、throw()、return() 的共同点

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是 让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式 next() 是将yield表达式替换成一个值

1
2
3
4
5
6
7
8
9
10
11
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(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined

1
2
3
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

throw() 是将yield表达式替换成一个throw语句

1
2
3
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

return() 是将yield表达式替换成一个return语句。

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。但如果有多个 Generator 函数嵌套,写起来就非常麻烦。ES6 提供了yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数

1
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
function g1(){
yield 2
yield 3
yield 4
}

function g2(){
yield 1
yield* g1()
yield 5
}

// -----------------------------------------
// 等同于:
function* g2(){
yield 1
yield 2
yield 3
yield 4
yield 5
}
// -----------------------------------------
// 也等同于:
function* g2(){
yield 1
for(let i of g1()){
yield i
}
yield 5
}

作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写。

1
2
3
4
let obj = {
myGeneratorMethod: function* () {
// ··· }
};
1
2
3
4
5
let obj = {
* myGeneratorMethod() {
···
}
};

Generator 函数的this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法

1
2
3
4
5
6
7
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
  1. 如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象
1
2
3
4
5
6
7
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
// 上面代码中,Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。
  1. Generator 函数也不能跟new命令一起用,会报错。
1
2
3
4
5
6
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
  1. 让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
    下面代码中,首先是F内部的this对象绑定obj对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次next方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
  1. 执行的是遍历器对象f,但是生成的对象实例是obj,将这两个对象统一呢:一个办法就是将obj换成F.prototype
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

再将F改成构造函数,就可以对它执行new命令了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

Generator 与状态机

Generator 是实现状态机的最佳结构。

比如,下面的clock函数就是一个状态机:

1
2
3
4
5
6
7
8
let ticking = true;
let clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}

clock函数一共有两种状态(TickTock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现:

1
2
3
4
5
6
7
8
let clock = function* () {
while (true) {
console.log('Tick!');
yield;
console.log('Tock!');
yield;
}
};

上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改) 、更符合函数式编程的思想,在写法上也更优雅

Generator与协程(coroutine)

  • 由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
  • Generator 函数是 ES6 对协程的实现,但属于不完全实现 :
    • Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。
    • 如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
    • 如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

Generator与上下文

  • JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。
  • 然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
  • 这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境一旦遇到yield命令,就会暂时退出堆栈但是并不消失,里面的所有变量和对象会冻结在当前状态等到对它执行next命令时,这个上下文环境又会重新加入调用栈冻结的变量和对象恢复执行

1
2
3
4
5
6
7
8
9
function* gen() {
yield 1;
return 2;
}
let g = gen();
console.log(
g.next().value,
g.next().value,
);

上面代码中,第一次执行g.next()时,Generator 函数gen的上下文会加入堆栈,即开始运行gen内部的代码。等遇到yield 1时,gen上下文退出堆栈,内部状态冻结。第二次执行g.next()时,gen上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

自动执行Generator函数

我们可以借助 Promise + Generator 可以手动控制异步代码和同步代码的执行顺序了,可是这么写目前依然存在一个很严峻的问题——类似于“回掉地狱”,代码层次不清晰等。

1
2
3
4
5
6
g.next().value.then(() => {
g.next().value.then(() => {
g.next()
// ......
})
})

Thunk函数

Thunk 函数是一种用于延迟执行的函数包装器,通常用于处理异步操作或生成器函数的执行。Thunk 函数的目标是将一个函数的执行推迟到稍后的时间,以便在需要时再执行。在 JavaScript 中,Thunk 函数通常是一个带有回调函数的函数包装器。

如下例子:A函数执行完毕,递归进去执行 B函数,B函数执行完毕,递归进去执行 C函数。 这样一来,我们就通过打造一个 Thunk 函数和一个 Thunk 函数执行器(run)来实现了让 Generator 函数自动执行下一次层的 next()

1
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
62
63
64
65
66
67
68
69
70
// 异步函数 A、B 和 C,现在它们接受一个回调函数作为参数,并在异步操作完成后调用回调函数。
function A(callback){
setTimeout(() => {
console.log('异步a');
callback();
}, 1000);
}

function B(callback){
setTimeout(() => {
console.log('异步b');
callback();
}, 500);
}

function C(){
setTimeout(() => {
console.log('异步c');
callback()
}, 100);
}

// Thunk函数:
function SimpleThunk(fn){
return function(callback){
fn(function (result){
callback(result)
})
}
}

// 使用Thunk函数自动执行生成器函数
function run(generator){
const g = generator()

function iterator(g){
// iterate(g); 执行了第一个 g.next(),这就开启了 yeild simpleThunk(A); 的执行,
// 得到的仍然是一个函数体,所以 const { value, done } = g.next(); 中value还是一个函数
const {value, done} = g.next()

if(done){
return
}

if(typeof value === 'function'){
// 调用 value() 带来了 函数A 的执行,A在1秒钟之后才会调用自己内部的 callback,
// 这就导致了 A函数没有执行完毕的话,iterate(g) 这一处的递归就无法开始
value(()=>iterator(g))
} else {
throw new Error('Generator函数应该返回一个Thunk函数')
}
}

iterator(g)
}

// 生成器函数
function* generator(){
yield SimpleThunk(A)
yield SimpleThunk(B)
yield SimpleThunk(C)
}

// 使用run函数自动执行生成器函数
run(generator)

// 输出:
// 异步a
// 异步b
// 异步c

co模块

除了 Thunk 函数的方式,还有一个方法可以实现 Generator 的自动执行,就是 co 模块

co 模块是大佬 TJ Holowaychuk封装的一个库,一个用于控制生成器函数执行的库,它允许你以同步的方式编写异步代码,使得生成器函数内部的异步操作看起来像同步代码一样。

co 模块的实现基于 Promise 和生成器函数的特性,它自动迭代生成器并处理 Promise 对象的返回值。

用法

原理

和Thunk函数一样同样是使用递归,不过 co 借助了 Promise中的 then 方法,所以需要使用者注意 yeild 后面的内容一定要返回一个 Promise 对象当上一个 yeild 执行完毕且状态变更为 fulfilledthen才能执行,也就才能走进下一层的递归

async/await 的实现就是由 Generator + Promise + co的手段 来封装的

Thunk和co的区别

co 和 Thunk 函数都有各自的用途和优势,选择使用哪个取决于你的具体需求和代码结构 :

  • co 更适合复杂的异步控制流程
  • 而 Thunk 函数更适合将异步操作封装成可延迟执行的函数。

当然,实际开发过程中,肯定是直接 async/await呀!

1. 用途

  • co 函数通常用于协调和管理异步操作的流程,使得异步操作看起来像同步代码一样执行。它通常与生成器函数结合使用。
  • Thunk 函数主要用于封装异步操作,将其包装成一个函数,以便在需要时延迟执行。Thunk 函数通常用于构建异步流程控制工具。

2. 执行方式

  • co 函数是一个库或工具,它使用 Promise 和生成器函数的协作来实现异步控制。co 内部会自动执行生成器函数,并管理异步操作的执行流程。
  • Thunk 函数是一个函数包装器,它接受回调函数作为参数,并通常需要手动调用来执行。Thunk 函数的执行需要显式地调用,而不像 co 那样自动进行异步流程控制。

3. 使用场景

  • co 函数适用于较复杂的异步流程控制,例如需要按顺序执行多个异步操作、处理错误等情况。它在管理多个异步任务时非常有用
  • Thunk 函数通常用于构建异步库或处理单一异步操作的情况,它更侧重于将异步操作封装成可延迟执行的函数,以便在需要时执行。

Generator应用

异步操作的同步化表达

1
2
3
4
5
6
7
8
9
10
11
12
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();

上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while(!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
}
let n = numbers()
// 使用yield表达式可以手动逐行读取文件。
n.next()
n.next()
n.next()
...
n.next()

控制流管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}
}

作为数据结构

因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口

1
2
3
4
5
6
7
8
9
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}

for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}

等价于:

1
2
3
4
5
6
7
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}

状态模式的语法糖

如果我们要自己去写状态模式的代码的话,会显得比较繁琐,相当于要自己去切换上下文,而有了Generator我们就可以直接利用Generator的能力为我们进行状态切换了。

频繁切换状态 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function* func() {
while (true) {
yield console.log("红灯亮起");
yield console.log("绿灯亮起");
yield console.log("黄灯闪烁,红灯即将亮起");
}
}

const light = func();

function start(immediate) {
setTimeout(() => {
light.next();
start();
}, 1000);
immediate && light.next();
}

作为迭代器的语法糖

请添加某些代码使得以下代码正常运行:

1
2
3
const obj = { a: 1, b: 2 } 
// 在此处添加你的代码
const [a, b] = obj;

这个题是要obj对象补迭代器 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
obj[Symbol.iterator] = function () {
const keys = Object.keys(this);
let idx = 0;
const _this = this;
return {
next() {
const i = idx++;
return {
value: _this[keys[i]],
done: i >= keys.length,
};
},
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个Generator,读取目标对象的
function *objIterator(obj) {
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
yield obj[key];
}
}
}

const obj = { a: 1, b: 2 };

obj[Symbol.iterator] = function() {
return objIterator(this)
}

// 在此处添加你的代码
const [a, b] = obj;