写在前面的话,说真的ES6的每一章节都超级长啊,这一块的学习我用了两天时间才完成。如果不赶紧把总结写出来,我过一段时间之后又会非常快的忘记掉。所以要赶紧记下来。
- 简介
- next方法的参数
- for...of循环
- Generator.prototype.throw()
- Generator.prototype.return()
- next(),throw(),return()的共同点
- yield * 表达式
- 作为对象属性的Generator函数
- Generator函数的this
- 含义
- 应用
上面是学习这一章的时候的目录,我们就按这个来总结,争取短点,把事说明白了就行,不明白的可以直接去书,那么我们开始吧。
1. 简介
Generator 函数是 ES6 提供的一种异步编程解决方案.
我们看一下代码:
function * helloWorldGenerator(){
yield 'hello';
yield 'world';
return 'ending'
}
var hw = helloWorldGenerator();
它有多种理解角度,一种是状态机,封装了多个内部状态,但是上面的例子并不好理解。
另一种是遍历器对象生成函数,返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
它有两个特征。一个是function关键字后面会带一个*
号,还有就是函数体内部会使用yield
表达式,定义不同的内部状态,(yield
在英语里的意思就是"产出")
Generator
函数的调用和普通函数一样,但是不同的是,调用后并不会立即得到值,而是返回一个遍历器对象。然后需要通过遍历器对象的next
方法来遍历yield
后面的表达式。
每一次调用next
方法,都会在yield
表达式后面暂停下来,然后得到yield
表达式后面的值,返回到遍历器对象的value
中。然后在重复上面的过程!
Generator
函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
1.第一次调用,返回yield
后面表达式的值,value
是hello
,done
是false
表示还没有执行完。
2.第二次调用,返回yield
后面表达式的值,value
是world
,done
是false
表示还没有执行完。
3.第三次调用,返回return
后面表达式的值,value
是ending
,done
是true
表示已经执行完了。
4.第四次调用,value
是undefined
,done
是true
表示执行完了。
总结一下:调用'Generator'函数,返回一个遍历器对象,代表的是Generator
函数的内部指针。每次调用遍历器对象的next
方法,会返回一个包含value
和done
两个属性的对象。value
属性表示当前的内部状态的值,yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
yield表达式
Generator
函数返回的遍历器对象,只有调用了next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停的标志。
遍历器对象的next
方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是yield
表达式后面的表达式,只有调用了next
方法,内部执行指向该语句时才会执行。因此等于为 JavaScript
提供了手动的“惰性求值”(Lazy Evaluation)
的语法功能。
function * gen(){
yield 123+456;
}
yield表达式与return语句的区别:
yield
表达式与return
语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield
,函数暂停执行,下一次再从该位置继续向后执行,而return
语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return
语句,但是可以执行多次(或者说多个)yield
表达式。正常函数只能返回一个值,因为只能执行一次return;Generator
函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator
生成了一系列的值,这也就是它的名称的来历(英语中,generator
这个词是“生成器”的意思)。
yield表达式只能在Generator函数中,用在其他地方会报错。
(function(){
yield 1;
})();
// SyntaxError: Unexpected number
与Iterator接口的关系
在Symbol
章节中:
任意一个对象的Symbol.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属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。
Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
2.next方法的参数
在这里我们要首先记住一点,yield
表达式本身是没有返回值的,或者说返回的是undefined
。看到这里我们一定会有疑惑,就是为什么会没有返回值呢,那上面我们写的代码里hw.next().value
这不是返回值吗,调用的第一个还返回了一个hello
呢,其实这里说的么有返回值不是调用next()
方法后没有返回值,而是在代码内部里面内没有,比如说
function * foo(){
var g = yield 'hello';
console.log(g);
}
在这个例子里,我们的函数执行完next
方法以后,暂停在yield
函数体后面,g
并没有被我们赋值,当我们调用第二次next
方法的时候,也就是从上一次暂停地方开始执行的时候,如果我们不把上一个next
的函数调用完返回值传递回去,让它把这个值赋值给g
的话,那么g
就是undefined
,所以这里引入了next
参数,就是为了给暂停以后,传值用的。
理解了上面就可以了!
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 }
看一下,为什么会输出这样的结果呢??
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()) {
console.log(v);
}
// 1 2 3 4 5
上面的代码中for
循环依次遍历5个yield
表达式的值。但是,它不会遍历return
的值,在遍历的过程中,如果碰到done
属性为true
时,表明已经完成,那么它的遍历就会结束,并且不会反悔done
属性为true
的对象。所以只打印了1~5,没有6.
下面是用Generator
函数和for...of
循环,实现斐波那契数列的例子。
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
循环,可以写出遍历任意对象的方法。原生的JavaScript
对象没有遍历接口,没有办法使用,我们可以通过Generator
函数为它加上这个接口。
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
加上遍历器的另一种写法是,将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
看到这里,我们就知道了,除了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
知识被串联起来的感觉。
4.Generator.prototype.throw()
我们先说一下这个是干嘛的,我们看上面的标题Generator.prototype.throw()
就知道,这是一个遍历器对象的方法,和next
方法,还有后面要介绍的return
方法一样,都是遍历器对象的方法。
它的主要作用是抛出错误,在体外抛出,在体内捕捉。
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
调用throw
方法抛出错误a
,可以看到打印的数据 是函数体内部捕获,由于函数体内部已经执行过捕获错误,所以第二天抛出错误的时候,不在捕获,交给了外部的处理。
如果捕获的错误在内部和外部都没有处理,则程序终止执行。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
上面代码中,g.throw
抛出错误以后,没有try...catch
代码块可以捕获这个错误,导致程序报错,中断执行。
thorw
方法捕获以后,会附带执行下一条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
g.next() // c
可以看到g.throw
抛出错误以后,b
也被打印出来了,就相当于自动执行了一次g.next
方法。所以,只要函数体内布置了try...catch
方法,那么遍历器对象调用throw
方法抛出的错误,就不会影响下一次遍历,影响程序的执行。
throw
和g.throw
方法是无关的,两者互不影响。
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
上面代码中,throw
命令抛出的错误不会影响到遍历器的状态,所以两次执行next方法,都进行了正确的操作。
记住一点,如果遍历器对象调用throw
方法,而函数体内部没有try...catch
进行处理,那么就不会在执行下去了,也就是已经执行完成。下一次调用next
方法,将返回一个value
属性等于undefined
,done
属性等于true
的对象。
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 函数就已经结束了,不再执行下去了。
5. Generator.prototype.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 }
遍历器对象调用retrun
方法后,传入了参数foo
,所以返回值的value
属性就是retrun
返回的参数foo
.并且Generator
函数的遍历终止了,返回值的done
属性为true
,以后再调用next
方法,done
属性总是返回true
.
如果return
方法调用时,不提供参数,则返回值的value
属性为undefined
.
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return() // { value: undefined, done: true }
如果Generator
函数内部有try...finally
代码块,那么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
方法。
6. next()、throw()、return()、的共同点
其实他们三个方法本质上是同一件事,都是遍历器对象的方法,作用都是让Generator
函数恢复执行,并且使用不同语句替换yield
表达式。
next()
是将yield
表达式替换成一个值。
throw()
是将yield
表达式替换成一个throw
语句。
return()
是将yield
表达式替换成一个return
语句。
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;
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
7.yield * 表达式
这个表达式用来在Generator
函数内部调用另一个Generator
函数。看一个例子。
普通的调用:
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
普通调用,没有效果,这里就需要用到yield *
表达式,用来一个Generator
函数里面调用另一个Generator
函数。
let delegatedIterator = (function*(){
yield 'Hello!';
yield 'Bye!';
})();
let delegatingIterator = (function*(){
yield 'Greetings!';
yield * delegatedIterator;
yield 'ok,bye!';
})();
for(let value of delegatingIterator){
console.log(value);
}
// "Greetings!
// "Hello!"
// "Bye!"
// "Ok, bye."
这里用到了立即执行函数,就相当于我们直接调用赋值给了delegatedIterator
,相当于这个操作var g = numbers();
,好,我们在delegatingIterator
这个函数中调用了delegatedIterator
这个遍历器对象。我们可以看到实际的效果是直接把delegatedIterator
中的yield
表达式的值打印了出来。
在一个generator
函数里通过yield *
就相当于在这个函数里通过for...of
循环遍历了另一个generator
函数体(没有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;
}
}
如果没有return
,yield *
表达式就是for...of
一种简写,完全可以用来后者替代前者。
如果有return
语句,则需要用var value = yield * iterator
的形式获取return
语句的值
实际上yieldb *
后面更的表达式,只要这个表达式支持遍历器,只要有Iterator
接口,那么就可以遍历,比如数组
、字符串
。
数组
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
字符串
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}
上面代码在第四次调用next
方法的时候,屏幕上输出了,这是因为foo
函数通过return
语句,把值给了bar
;
yield *
可以很方便地取出所有嵌套数组的成员。
function * iterTree(tree){
if(Array.isArray(tree)){
for(let i = 0; i < tree.length;i++){
yield * iterTree(tree);
}
}else{
yiled tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
还有遍历二叉树,这些代码都值得自己写一遍。
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
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']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
8.作为对象属性的Generator函数
如果一个对象的属性是Generator
函数,可以简写成下面的形式
let obj = {
* myGeneratorMethod() {
···
}
};
完整形式如下:
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
9.Generator函数的this
一句话来总结:你不可以把Generator
函数当做普通的构造函数。
为什么呢?
Generator
函数返回一个遍历器,而这个遍历器对象就是Generator
函数的实例,它也继承了Generator
函数的prototype
对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
上面代码里,Generator
函数g
返回一个遍历器对象obj
,是g
的实例,而且也继承了g.prototype
,但是你不能把它当做普通对象,并不会生效,因为它返回的总是遍历器对象,而不是this
对象。
function * g(){
this.a = 11;
}
let obj = g();
obj.next();
obj.a //undefined
上面的代码中,我们给g
在this
对象上添加了一个属性a
,但是obj
对象拿不到这个属性。
Generator
函数也不能更new
命令一起用,会报错。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
上面的代码中,new
命令更构造函数一起使用,结果报错,因为F不是构造函数。
那么问题来了,我们想让一个Generator
函数返回一个正常的对象实例,既可以用next
方法,又可以获得正常的this
?
obj
换成F.ptototype
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命令了。
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
10.含义
Generator与状态机
一个普通的状态机是这样子的
var ticking = true;
var clock = function() {
if (ticking)
console.log('Tick!');
else
console.log('Tock!');
ticking = !ticking;
}
上面的clock
函数有两种状态,Tick
和Tock
,没运行一次,就改变一次状态,用Generator
实现。
var clok = function*(){
while(true){
console.log('Tick!');
yield;
console.log('Tock!');
yield
}
}
上面的状态机比ES5
的实现更优雅,更简洁,我们并没有保存状态的外部变量,因为它本身就包含了一个状态信息,也就是目前是否处于暂停状态。这样更加简洁,更安全,状态不会被非法修改。
11.应用
这里介绍了四种应用场景,我们依次来看一下。
(1) 异步操作的同步表达----不需要在写回调函数
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
可以看到,我们在用同步的写法写异步的操作,而回调的操作直接写在了yield表达式的后面。
看这个例子:
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();
看到这里我有一个疑问,为什么request
方法内可以调用it.next(response)
,我们知道yield
表达式是没有返回值的,它的返回值是undefined
,如果我们我想让下面的代码能够执行下去,就需要把response
当做传递回去,那么为什么可以调到it
呢,这个实例在下面,这里你就应该想到Generator
函数的执行顺序了,我们实例话以后并不会执行里面的程序,也就是var it = main()
并没有执行下面的代码,而是停在 在yield
那里,好了,想到这个就明白了。
(2) 控制流管理
这里大家可以看原文,我就不写了!
(3) 部署 Iterator 接口
我们可以在任意对象上部署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 };
for (let [key, value] of iterEntries(myObj)) {
console.log(key, value);
}
// foo 3
// bar 7
作为数据结构
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');
}
上面代码就是一次返回三个函数,但是由于使用了Generator
函数,导致可以像处理数组那样,处理这三个返回的函数。
for(task of doStuff){
}
实际上,我们也可以用数组来模拟这种写法。
function doStuff() {
return [
fs.readFile.bind(null, 'hello.txt'),
fs.readFile.bind(null, 'world.txt'),
fs.readFile.bind(null, 'and-such.txt')
];
}
上面的函数,一样也可以用for...of循环处理。Generator
使得数据或者操作,具备了类似数组的接口。
终于写完了,我以为会写的简短点,可是发现后来,如果需要把事情说明白,你就得写这么多。还有一些是照搬原来的书的部分,只是添加了一些我自己的理解。写完以后不是说已经完事了,最主要的是,温故而知新