简单谈谈
Generator
:状态机,封装了多个内部状态。执行Generator
返回一个遍历器对象,用于变量每一次的状态。
简单来说,一个 Generator
函数,就像一段楼梯,规定了楼梯的步数以及没一步的状态,调用它就生成了一段楼梯,每走一步都会有具体的执行内容以及返回值,并且只能往前走( yield
),或是走到头了( return
)。
Generator
函数的标志就是 function
后面带 *
号。
一个简单的例子:
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
let hw = helloWorldGenerator();
该 Generator
函数规定了 3
级台阶(两个 yield
和一个 return
),当函数调用的时候,生成了一段楼梯,接下来就是往上走
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
例子中 helloWorldGenerator
函数生成的 hw
(楼梯)一共可以走 3
步,在调用 3
次 next
方法调用结束之后,就会固定返回 { value: undefined, done: true }
。
返回值
如上代码所示,next
调用后的返回值中的 value
即为 yield
后跟语句的值,done
代表楼梯是否走完,当 Generator
函数碰到 return
语句时,done
即为 true
。那要是没有 return
咋办?JavaScript
中每个函数都有默认的返回值:undefined
。
yield
yield
:Generator
函数内部的一个状态,与 yield
息息相关的是 next
方法。
next
的机制:
- 调用
next
方法,函数执行到第一个状态(yield
)并暂停,返回{ value: yield 后表达式的值, done: false }
。 - 再次调用,函数恢复执行,直到执行到下一个状态(
yield
)并暂停,返回{ value: yield 后表达式的值, done: false }
;若没有遇到下一个状态,则走到return
返回{ value: return 后表达式的值, done: true }
。 - 接着调用,回到第二步。
- 如果再次调用,直接返回
{ value: undefined, done: true }
。
试着想想 JavaScript
中函数的执行过程,在 Generator
函数出现之前,函数都是一股脑的从头执行到末尾,而 Generator
的出现让函数内部实现了一种暂停的效果,可以让函数一步一步的执行。
一些注意点
yield
只能在Generator
函数中使用。- 即使仅在
Generator
函数中,yeild
也仅仅只能在函数的一级作用域中使用,比如不能再forEach
中使用。 yeild
表达式如果在另一个表达式中使用,必须放在圆括号中
以下是是一些常见的错误:
// 示例一
(function (){
yield 1; // 错误:不能在非 `Generator` 函数中使用。
})();
// 示例二
let arr = [1, [[2, 3], 4], [5, 6]];
let flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item); // 错误:仅能在函数的一级作用域内使用。
} else {
yield item; // 错误:仅能在函数的一级作用域内使用。
}
})
};
// 示例三
function* demo() {
console.log('Hello' + yield); // 错误:在一个表达式中使用 yield 必须放在括号中
console.log('Hello' + yield 123); // 错误:在一个表达式中使用 yield 必须放在括号中
console.log('Hello' + (yield)); // 正确
console.log('Hello' + (yield 123)); // 正确
}
与 Iterator 的关系
通过上面内容,可以很自然的将 Generator
函数与 Iterator
接口挂上关系。Iterator
的定义:
部署在对象的Symbol.iterator
上,调用该函数会返回该对象的一个遍历器对象,该对象拥有next
方法。
而调用 Generator
函数,就能给我们返回一个拥有 next 方法的对象。
所以在 Iterator
中我们说的内容,用 Generator
函数我们都可以实现,这里就不过多深入了。
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回 undefined
。next
方法可以带一个参数,该参数就会被当作前个状态(yield
)表达式的值。
注意是前一个状态的值!第一次调用 next
方法时,即使传入参数也毫无意义。
其实也很好理解:在调用 next
时,此时 Generator
函数状态时停留在上一个 yield
处的,而 next 调用的参数就相当于给那个状态一个具体的值,当第一次调用 next
时,由于 Generator
函数还么有任何状态,因此也就没用了。
一个简单的例子:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
let a = foo(5);
a.next();
// { value: 6, done: false }
a.next();
// { value: NaN, done: false }
// 推到过程:(yield (x + 1)) => undefined => 2 * undefined => NaN => NaN/3 => NaN
a.next();
// { value: NaN, done: true }
// 推到过程:yield (y / 3) => undefined => 5 + NaN + undefined => NaN
let b = foo(5);
b.next();
// { value: 6, done: false }
b.next(12);
// { value: 8, done: false }
// 推到过程:(yield (x + 1)) => 12 => 2 * 12 => 24 => 24/3 => 8
b.next(13);
// { value: 42, done: true }
// 推到过程:yield (y / 3) => 13 => 5 + 24 + 13 => 42
Generator.prototype.throw
Generator
函数返回的遍历器对象,拥有throw
方法,可以在函数体外抛出错误,然后在Generator
函数体内捕获。
throw
方法可以认为是吧 yield
语句替换成一个 throw
语句。
let g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获:', e);
}
}
let i = g();
i.next();
try {
i.throw('a'); // i 处于第一个状态,把第一个 yield 替换为了 throw。
i.throw('b'); // i 出于第二个状态,但 g 却并没有实现,因此直接抛错。
} catch (e) {
console.log('外部捕获:', e)
}
// 内部捕获: a
// 外部捕获: b
**注:**如果 throw
对应的 yield
语句在 Generator
函数内部没有进行 try...catch
的话,是会向外部抛出的,就如例子中的 b
。
就像程序执行的那样,如果出错没有 try...catch
的话,是会往上抛的。
throw
方法,主要是为了在 Generator
函数外提供一个正常的错误提示机制。
Generator.prototype.return
return
方法作用和 throw
差不多,只是它把 yield
语句换成了 return
语句。
function* gen() {
yield 1;
yield 2;
yield 3;
}
let g = gen();
g.next();
// { value: 1, done: false }
g.return('foo');
// { value: 'foo', done: true }
g.next();
// { value: undefined, done: true }
// 由于上一步已经 return 故没有了下一步。
next throw return 共同点
next
、throw
、return
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator
函数恢复执行,并且使用不同的语句替换 yield
表达式。
next
是将yield
表达式替换成一个值。throw
是将yield
表达式替换成一个throw
语句。return
是将yield
表达式替换成一个return
语句。
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
就如同例子中的一样,foo
中的楼梯( yield
)并没有过加入到 bar
的楼梯上,而正常我们想要的效果应该是两个楼梯合并成一个楼梯,这时候,我们就需要使用到 yield*
表达式,如下所示
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// 'x'
// 'a'
// 'b'
// 'y'