Generator函数
Generator函数是ES6提供的一种异步变成解决方案,语法行为与传统函数完全不同。
Generator函数有多种理解角度,语法上,可以将Generator函数理解为状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,可以依次遍历Generator函数中的每一个状态。
Generator函数有两个特征:一是,function关键字紧跟一个*号;二是,函数体内部使用yield表达式,定义不同的内部状态。
yield能返回跟在语句后面的表达式的值。
function* helloWorldGenerator() {
yield "hello";
yield "world";
return "ending";
}
var it = helloWorldGenerator();
console.log(it.next());
console.log(it.next());
console.log(it.next());
如果helloWorldGenerator函数没有return ‘ending’,那么第三次调用next函数的返回值中,value = undefined.
// 星号位置,都可以
function* generator() {}
function* generator() {}
function* generator() {}
function* generator() {}
yield 表达式
yield表达式就是Generator函数暂停执行的标志。
遍历器的next方法运行如下:
- 第一次执行next(),遇到yield表达式,就暂停后面的操作,并将紧跟yield后的表达式的值,作为返回对象value的值。
- 下一次调用next方法时,再继续执行,直到遇到下一个yield表达式。
- 如果没有遇到yield表达式,就一直运行到函数结束,直到return语句为止,并将return后的表达式的值,作为返回对象的value属性的值。
- 如果没有return语句,那么返回对象的value属性的值为undefined。
与Iterator接口的关系
对于任意一个对象的System.iterator方法,等于该对象的遍历器生成函数,调用该函数 会返回该对象的一个遍历器对象。
var myIterator = {};
myIterator[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
yield 4;
};
console.log([...myIterator]);
next方法的参数
yield表达式本身没有返回值,或者说总是undefined。next()方法可以带一个参数,该参数就会被当做上一个yield表达式的返回值。
该例子可以证明,yield返回值总是undefined,除非next方法传参。
function* f() {
for (var i = 0; true; i++) {
var reset = yield i;
console.log(reset);
if (reset) {
i = -1;
}
}
}
var g = f();
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
console.log(g.next());
// console.log(g.next(true));
下面这个例子说明 yield表达式本身是返回undefined的,除非next方法传参
function* foo(x) {
var y = 2 * (yield x + 1);
var z = yield y / 3;
return x + y + z;
}
var a = foo(5);
// 6
console.log(a.next());
NaN;
console.log(a.next());
NaN;
console.log(a.next());
var b = foo(5);
// 6
console.log(b.next());
8;
console.log(b.next(12));
// 42 y=24 z=13 x=5
console.log(b.next(13));
下面再来一个面试题,看看结果是什么
function* dataConsumer() {
console.log("started");
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return "result";
}
var it = dataConsumer();
// started
console.log(it.next());
// 1. a
console.log(it.next("a"));
// 1. b
console.log(it.next("b"));
for…of 循环
for…of循环能够遍历Generator函数运行时生成的Iterator对象,并且不需要调用next方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
let it = foo();
for (let key of it) {
console.log(key);
}
如果你运行这个代码就会发现,return的6没有输出,这是因为next方法返回对象的done属性为true时,for…of就会停止执行。
除了for…of外,扩展运算符(…),解构赋值和Array.form方法内部调用的,都是遍历器接口。这意味着他们都可以将Generator函数返回的遍历器对象作为参数。
function* foo() {
yield 1;
yield 2;
yield 3;
return 4;
yield 5;
}
// 扩展运算符
console.log([...foo()]);
// Array.from
console.log(Array.from(foo()));
// 解构赋值
var [a, ...b] = foo();
console.log(a);
console.log(b);
Generator.prototype.throw()
Generator函数返回的遍历器对象,都有一个throw方法,可以在函数体外跑出错误,然后在Generator函数体内部捕获。
function* g() {
try {
yield;
} catch (e) {
console.log("函数内部捕获错误", e);
}
return 123123;
}
var it = g();
console.log(it.next());
try {
it.throw("a");
it.throw("b");
it.throw(new Error("记住,这么抛错误!"));
} catch (e) {
console.log("函数体外捕获错误", e);
}
Generator.prototype.return()
Generator函数返回的遍历器对象,还有个return()方法,可以返回给定的值,并且终结遍历Generator函数。
function* foo() {
yield 1;
yield 1;
yield 1;
return 4;
}
var it = foo();
console.log(it.next());
console.log(it.return(666));
如果Generator函数内部有try…finally代码块,且正在执行try块,那么return方法会推迟到finally块执行完后再执行
// 即使return 也会先执行finally
function* foo2() {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
}
var it2 = foo2();
console.log(it2.next());
// 此处注意,要是没有执行到try内部,则finally不会执行。
// console.log(it2.next())
console.log(it2.return(555));
console.log(it2.next());
next()、throw()、return() 的共同点
他们的作用都是让Generator函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换为一个值。
function* foo() {
let res = yield 1;
return res;
}
var it = foo();
console.log(it.next());
console.log(it.next(123123));
第二个next(123123)相当于把yield表达式的值替换为123123,如果next参数为空,相当于替换为undefined;
throw()是将yield表达式替换成一个throw语句。
var it2 = foo();
console.log(it2.next());
console.log(it2.throw(new Error("又错了!")));
return()是将yield表达式替换成一个return语句。
var it3 = foo();
console.log(it3.next());
console.log(it3.return(666));
yield* 表达式
在一个Generator函数内部,调用另一个Generator函数,需要在前者的函数体内,手动完成遍历
function* foo() {
yield 1;
yield 2;
return 3;
}
function* bar() {
yield "x";
for (let i of foo()) {
console.log(i);
}
}
for (let i of bar()) {
console.log(i);
}
如果在bar函数内部调用多个Generator函数,处理就会变得特别麻烦,所以出现了yield*表达式。
// yield*版本
function* bar2() {
yield "x";
yield* foo();
yield "y";
return "zzz";
}
let it2 = bar2();
console.log(it2.next());
console.log(it2.next());
console.log(it2.next());
console.log(it2.next());
console.log(it2.next());
// 等同于
function* bar3() {
yield "x";
yield 1;
yield 2;
yield "y";
return "zzz";
}
console.log("\n");
// 等同于
function* bar4() {
yield "x";
for (let v of foo()) {
yield v;
}
yield "y";
return "zzz";
}
for (let v of bar4()) {
console.log(v);
}
再看下面的一个例子,outer2用了yield*, outer1没用,结果是outer1返回了一个遍历器对象,outer2返回了遍历器对象内部的值。 从语法的角度看,如果yield后面跟着一个遍历器对象,那么需要在yield后加星号,表明它返回的是遍历器对象。这称为yield*表达式
function* inner() {
yield "hello";
}
function* outer1() {
yield "open";
yield inner();
yield "close";
}
var it1 = outer1();
console.log(it1.next());
console.log(it1.next());
console.log(it1.next());
console.log(it1.next());
console.log(`\n outer2 `);
function* outer2() {
yield "open";
yield* inner();
yield "close";
}
var it2 = outer2();
console.log(it2.next());
console.log(it2.next());
console.log(it2.next());
console.log(it2.next());
再看下面这段代码,delegratingIterator是代理者,delegratedIterator是被代理者。由yield* delegratedIterator语句得到的是一个遍历器,所以要用星号表示。 运行结果就是使用了一个遍历器,遍历了多个Generator函数,有递归的效果。
let delegratedIterator = (function* () {
yield "hello";
yield "bye";
})();
let delegratingIterator = (function* () {
yield "Greeting";
yield* delegratedIterator;
yield "ok, bye";
})();
for (let value of delegratingIterator) {
console.log(value);
}
作为对象属性的Generator函数
let obj = {
*myGeneratorMethods() {
yield 3;
yield 4;
return 5;
},
};
for (let value of obj.myGeneratorMethods()) {
console.log(value);
}
console.log(`\n 另一种方式声明对象方法`);
// 上面的写法等同于下面
let obj2 = {
myGeneratorMethods: function* () {
yield 1;
return 2;
},
};
for (let value of obj2.myGeneratorMethods()) {
console.log(value);
}
Generator函数的this
Generator函数总是返回一个遍历器,ES6规定这个遍历器是Generator函数的实例,也继承了Generator函数的prototype对象上的方法。
function* foo() {}
foo.prototype.hello = function () {
return "hello";
};
let obj = foo();
console.log(obj instanceof foo);
console.log(obj.hello());
上述代码表明,Generator函数foo返回的遍历器obj,是foo的实例,同时也继承了foo.prototype。
含义
Generator与状态机
var ticking = true;
var clock = function () {
if (ticking) {
console.log("tick");
} else {
console.log("tock");
}
ticking = !ticking;
};
clock();
clock();
clock();
// Generator版本
function* clock2() {
while (true) {
console.log("tick");
yield;
console.log("tock");
yield;
}
}
var it = clock2();
console.log(it.next());
console.log(it.next());
console.log(it.next());
Generator实现与ES5的实现相比,少了外部保存状态的ticking变量,更简洁、安全。Generator之所以不用保存状态,是因为它本身就包含了一个状态信息, 即目前是否出于暂停状态。