文章目录
Generator
基本概念
- 形式上,
Generator
函数是一个普通函数,但是有两个特征。一是,function
关键字与函数名之间有一个星号
;二是,函数体内部使用yield
表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next() // { value: 'hello', done: false }
hw.next() // { value: 'world', done: false }
hw.next() // { value: 'ending', done: true }
hw.next() // { value: undefined, done: true }
//上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield
//表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句
//(结束执行)。
- 如果一个
对象
的属性是Generator
函数,可以简写成下面的形式。
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator)
-
下一步,必须调用遍历器对象的
next方法
,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)
为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行
。 -
总结一下,调用
Generator 函数
,返回一个遍历器对象
,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next
方法,就会返回一个有着value
和done
两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。
yield表达式
yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
- 由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。
yield表达式就是暂停标志
。
-
遍历器对象的next方法的运行逻辑如下。
-
(1)遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值
。 - (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
-
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到
return语句
为止,并将return语句后面的表达式的值,作为返回的对象的value属性值
。 -
(4)如果该函数没有return语句,则返回的对象的
value属性值为undefined
。
// 下面是一个 yield 应用的例子
var arr = [1, [[2, 3], 4], [5, 6]];
// 重新定义一个新的平铺函数,即将嵌套的数组全部拆开 => arr.flat(n)
var flat = function* (a) {
var length = a.length;
for (var i = 0; i < length; i++) {
var item = a[i];
if (typeof item !== 'number') {
// 抛出一个 Generator 函数
yield* flat(item);
} else {
yield item;
}
}
};
for (var f of flat(arr)) {
console.log(f);
}
// 1, 2, 3, 4, 5, 6
- 另外,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
}
与 Iterator 接口的关系
- 由于
Generator
函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterato
r属性,从而使得该对象具有 Iterator 接口。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
// 此时的 myIterable 具备 iterator部署了接口,可以被扩展运算符遍历
next 方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
// 对比理解的例子:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next()
// Object{value:6, done:false}
a.next()
// Object{value:NaN, done:false}
a.next()
// Object{value:NaN, done:true}
// 出现 NAN 的原因是因为 yield的返回值为undefined,2*undefined=undefined
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
// 这里的12 替代了yield(x+1),所以 y= 24
b.next(13) // { value:42, done:true }
// 这里的13替代了 yield(y/3),所以 z= 13
注意,由于next方法
的参数表示上一个yield表达式的返回值
,所以在第一次
使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次
使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
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
// 没有出现 6 是因为 for...of循环有一个规则就是当 done=true的时候即刻停止
// 且不返回该对象的 value属性
利用for...of
循环,可以写出遍历任意对象(object
)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for…of循环,通过Generator
函数为它加上这个接口,就可以用了。
// 这就可以解决我们之前 Iterator 存在的问题
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
// proKeys => obj对象包含属性的数组 ['first', 'last']
// Reflect.ownKeys()方法返回一个由目标对象自身的属性键组成的数组。
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
除了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 函数体内捕获。throw方法
可以接受一个参数,该参数会被catch
语句接收,建议抛出Error对象的实例
。
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语句
//捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,
//不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数
//体外的catch语句捕获。
如果 Generator
函数内部没有部署try...catch
代码块,那么throw
方法抛出的错误,将被外部try...catch
代码块捕获。
var g = function* () {
//里面不做 try...catch 错误处理
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 外部捕获 a
// 抛出的第二个错误在第一个错误抛出时已经被中断了,不会再执行错误抛出
-
总结:
-
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
-
throw
方法抛出的错误要被内部捕获,前提是必须至少执行过一次next
方法。
-
- 只要部署了
try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历。
- 只要部署了
function* IndexError(){
let index = 0,
arr = [1, 2, 3]
try{
yield arr
}catch(err){
console.log(`${err} => 内部错误`)
}
yield ++index
}
let Something = IndexError()
Something.next() // {value:[1, 2, 3], done:false}
try{
Something.throw (new Error(`Error`))
}catch(err){
console.log(`${err} => 外部错误`) //{value:1, done:false}
}
Something.next() // {value:undefined, done:true}
Generator.prototype.return()
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
代码块,执行完以后,整个函数才会结束。
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 }
g.next() //{value:undefined, done:true}
//上面代码中,调用return()方法后,就开始执行finally代码块,不执行try里面
//剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回
//值。
yield* 表达式
yield*表达式
用来在一个 Generator
函数里面执行另一个 Generator 函数。
// 例子 bar函数内部调用 foo函数
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 省去了遍历 foo 函数的步骤,让代码更加得通俗易懂
// 我们也可以这么理解:yield*后面的 Generator 函数(没有return语句时),
//等同于在 Generator 函数内部,部署一个for...of循环。
如果yield*
后面跟着一个数组
,由于数组原生支持遍历器,因此就会遍历数组成员
。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
gen().next() // { value:"b", done:false }
gen().next() // { value:"c", done:false }
gen().next() // { value:"undefined", done:false }
不仅仅是数组,实际上任何数据结构只要有 Iterator 接口
,就可以被yield*
遍历。
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
// 对比两次的 next也可以证实我们上面的结论
read.next().value // "hello"
read.next().value // "h"
Generator 函数的 this
Generator
函数总是返回一个遍历器
,ES6 规定这个遍历器是Generator 函数的实例
,也继承了 Generator 函数的prototype
对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g(); // obj 继承了 g.prototype
obj instanceof g // true
obj.hello() // 'hi!'
-
但是,如果把
- (简单来说就是他的实例并不是一个正常的对象而是一个遍历器对象, this不指向遍历器对象)。
Generator函数
当作普通的构造函数,并不会生效,因为Generator返回的总是遍历器对象,而不是
this对象
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
Generator
函数也不能跟new
命令一起用,会报错。
function* F() {
yield this.x = 2;
yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
解决的办法:
function* gen() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F() {
return gen.call(gen.prototype);
// 将 gen()函数的 this指向 gen()的原型对象
}
var f = new F();
// 此时实例化F() 等同于实例化 gen()的原型对象
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