简介
Generator是ES6提供的一种异步编程的解决方案,语法行为和传统函数完全不一样,Generator函数有多种理解角度,从语法上来讲,它是一个状态机,内部封存着多个状态,那么执行Generator函数会返回一个遍历器(Iterator)对象。
从形式上来讲,Generator是一个普通函数,但是它有两个特征,一是funciton
和函数名
之间有个星号(*),而是函数内部使用yield
表达式,定义不同的内部状态。
基本用法
function* test(){
yield 1;
yield 2;
return 3;
}
var start=test();
如上代码所示,Generator函数test内部有两个yield表达式,即该函数有三个状态,1,2,return语句(结束执行)。
Generator函数的调用和普通函数调用一样,不同的时候,调用Generator函数的时候,并没有执行,也没有返回执行结果,而是返回一个指向内部状态的指针对象,也就是遍历器对象
。
那么,怎么执行它呢?这时候必须调用遍历器对象的next
方法。
console.log(start.next())//{value: 1, done: false}
console.log(start.next())//{value: 2, done: false}
console.log(start.next())//{value: 3, done: true}
可以看出每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value
属性表示当前的内部状态的值,是yield
表达式后面那个表达式的值;done
属性是一个布尔值,表示是否遍历结束。
简单的说,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
yield表达式
上面已经提过,yield表达式是一个暂停标记,每一个yield表达式代表着就是一种状态,而执行这种状态,需要调用遍历器的next
方法,下面咱们就通过一个例子讲下next
的运行逻辑。
function* test(){
console.log(1)
yield 1;
console.log(2)
yield 2;
console.log(3)
return 3;
}
var start=test();
console.log(start.next())
console.log(start.next())
console.log(start.next())
- 第一次执行next方法,代表Generator函数开始执行,那么首先打印出
1
,这时候遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值,那么第一个调用next函数的结果就是{value: 1, done: false}
- 第二次调用next方法,再继续往下执行,首先打印出
2
,遇到yield表达式时暂停,那么第二次结果{value: 2, done: false}
。 - 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值,然后把返回对象的done属性值置为
true
- 如果该函数没有return语句,则返回的对象的value属性值为undefined,done属性值依然为
true
。
上面例子,在控制台结果如下所示:
需要特别注意的是:
yield
表达式只能用在 Generator
函数里面,用在其他地方都会报错。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
另外,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
}
next方法的参数
yield
表达式本身是没有返回值的,或者说总是返回undefined
,但是next
方法可以带一个参数,可以当作上一个yield
表达式的返回值。
function* test(x){
var y=2*(yield x+1);
var z=yield y+2;
return x+y+z;
}
var start=test(1);
console.log(start.next())
console.log(start.next())
console.log(start.next())
以上这个例子,next
没有带参数,那么结果就是
可以看到,第二,三次next
的返回值为NaN
,是因为yield的返回值为undefined
,那么undefined加数组就等于NaN
,接下来,咱们再next方式传入参数,
function* test(x){
var y=2*(yield x+1);
var z=yield y+2;
return x+y+z;
}
var start=test(1);
console.log(start.next())
console.log(start.next(2))
console.log(start.next(3))
分析下:
- 调用Generator函数
test
,传入一个参数1,接下来执行遍历器的next
方法,第一次执行,遇到yield x+1
,返回结果{value:2,done:false}
- 第二次执行
next
方法,并传入参数2,这时候,2就是上一次yield
表达式的值,也就是yield x+1
的值,那么y就等于4,这时候遇到第二个yield
表达式,暂停,返回结果{value:6,done:false}
- 第三次执行
next
方法,并传入参数3,这时候,3就是上一次yield
表达式的值,也就是yield y+2
的值,那么z就等于3,这时候遇到return语句,结束执行,返回结果{value:8,done:false}
需要注意的是:
由于next
参数代表的是上一次yield
表达式的值,所以第一次执行next
方法,传递参数是无效的,从语义上来讲,第一次执行next
方法是用来启动遍历器对象的,所以不用带参数。
for…of循环
for...of
循环可以自动遍历 Generator
函数运行时生成的Iterator
对象,且此时不再需要调用next
方法
function* test(x){
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
yield 6;
return 7
}
for(let i of test()){
console.log(i)//1 2 3 4 5 6
}
上面代码细心的朋友会发现少打印了一个7,这是因为一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的7,不包括在for…of循环之中。
除了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
Generator.prototype.throw()
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var throwOperator = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = throwOperator();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
//内部捕获
//外部捕获
上面例子,遍历器对象i连续抛出两个错误,第一个由内部捕获,第二个由外部捕获,这是因为,第一次catch
执行过,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch
语句捕获。
需要注意几点:
- 遍历器对象的throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
- 如果调用了遍历器对象的throw方法,如果Generator函数内部没有
try..catch
,那么将会被外部的try..catch
捕获,如果都没有将报错,另外需要注意的是,不要混淆throw方法和遍历器对象的throw方法,前者是只能被外部的try..catch
捕获。 - 遍历器对象的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 g.next() // c
var gen = function* gen() { yield console.log('a'); yield console.log('b'); yield console.log('c'); } try { var g = gen(); g.next() // a g.throw() // b g.next() // c } catch (e) { console.log(e) } // a //"报错"
- throw命令与遍历器对象的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(); }
Generator.prototype.return()
Generator 函数返回的遍历器对象,还有一个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 }
需要注意的是:
如果 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();
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 }
yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
// 手动遍历 foo()
for (let i of foo()) {
yield i
}
yield 'y';
}
for(let i of bar()){
console.log(i)//x a b y
}
有没有发现很麻烦,所以ES6 提供了yield*
表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo()
yield 'y';
}
for (let i of bar()) {
console.log(i)//x a b y
}
需要注意的是:
如果被代理的 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方法时,遇到return,会提供一个返回值。
Generator函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype
对象上的方法。
function* g() { }
g.prototype.hello = function () {
console.log("hello")
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
但是,需要注意的是:
- 如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
function* g() { this.name="hty" } let obj = g(); console.log(obj.name)// undefined
- Generator函数不能和new一起使用,因为它并不是构造函数。
那么,有个问题,怎么既可以用next方法,又可以获得正常的this?最好的解决方案就是,把this绑到Generator函数的prototype
上。
function* g() {
this.name="hty"
yield this.sex="男"
yield this.age="25"
return this.lover="xxx"
}
let obj = g.call(g.prototype);
console.log(obj.next())//{value: "男", done: false}
console.log(obj.name)// "hty"
console.log(obj.sex)// "男"
console.log(obj.next())//{value: "25", done: false}
console.log(obj.age)// "25"
console.log(obj.next())//{value: "xxx", done: true}
console.log(obj.lover)// "xxx"
这里需要注意,只有执行next方法,才能把内部属性绑定到obj
上。