迭代器与生成器
ES新增的高级特性:迭代器和生成器。
关于迭代
循环是迭代机制的基础,迭代会在一个有序集合上进行。
迭代器模式
即可以把有些结构称为“可迭代对象",因为它们实现了正式的Iterable接口,而且可以通过迭代器Iterator消费。
基本上可以把可迭代对象理解成数组或集合这样的集合类型的对象。它们包含有限的元素,都具有无歧义的遍历顺序。
不过,可迭代对象不一定是集合对象,也可以是仅仅具有类似数组行为的其他数据结构,如计数循环,该循环中生成的值是暂时性地,但循环本身是在执行迭代。计数循环和数组都具有可迭代对象的行为。
迭代器是按需创建的一次性对象,每个迭代器都会关联一个可迭代对象。
可迭代协议
实现Iterable接口(可迭代协议)要求同时具备两种能力:支持迭代的自我识别能力和创建Iterator接口的对象的能力。 在ES中,意味着必须暴露一个属性作为“默认迭代器”,而且这个属性必须使用特殊的 Symbol.iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新的迭代器。
很多内置类型都实现了Iterator接口:
- 字符串;
- 数组;
- 映射;
- 集合;
- arguments对象;
- NodeList 等DOM集合类型。
let num = 1;
console.log(num[Symbol.iterator]); // undefined 没有实现迭代器工厂函数
// 实现Iterator接口的内置类型
let str = 'abc';
let arr = ['a', 'b', 'c'];
let map = new Map().set('a', 1).set('b', 2).set('c', 3);
let set = new Set().add('a').add('b').add('c');
let els = document.querySelectorAll('div');
// 这些类型都实现了迭代器工厂函数
console.log(arr[Symbol.iterator]); // f values() { [native code] }
....
// 调用这个工厂函数会生成一个迭代器
console.log(arr[Symbol.iterator] () ); // ArrayIterator {}
....
实际写代码过程种,无需显式调用这个工厂函数来生成迭代器。实现可迭代协议的所有类型自动兼容接收可迭代对象的任何语言特性,接收可迭代对象的原生语言特性包括:
-
for-of循环;
-
数组解构;
-
扩展操作符;
-
Array.from();
-
创建集合;
-
创建映射;
-
Promise.all()接收由期约组成的可迭代对象;
-
Promise.race()接收由期约组成的可迭代对象;
-
yield*操作符,在生成器中使用。
这些原生语言结构会在后台调用提供的可迭代对象的这个工厂函数,从而创建一个迭代器:
let arr = ['foo', 'bar, 'baz'];
// 数组解构
let [a,b,c] = arr;
console.log(a,b,c); // ['foo', 'bar', 'baz']
//扩展操作符
let arr2 = [...arr];
console.log(arr2); // ['foo', 'bar', 'baz']
// Array.from()
let arr3 = Array.from(arr);
console.log(arr3); // ['foo', 'bar', 'baz']
迭代器协议
迭代器API使用next()方法在可迭代对象中遍历数据,每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next(),无法知道迭代器的当前位置。
next()方法返回的迭代器对象IteratorResult包含两个属性:done和value,done是布尔值,表示是否还可以再次调用next()取得下一个值;value包含可迭代对象的下一个值(done为false),或者undefined(done为true)。
done:true状态称为“耗尽”。只要迭代器到达done:true,后续调用next()就一直返回同样的值了。
let arr = ['foo'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // {done: false, value : 'foo' }
console.log(iter.next()); // {done: true, value : undefined }
console.log(iter.next()); // {done: true, value : undefined }
每个迭代器都表示对可迭代对象的一次性有序遍历。不同迭代器的实例相互之间没有联系,只会独立地遍历可迭代对象。
如果可迭代对象在迭代期间被修改了,那么迭代器也会反映相应的变化。
注意,迭代器维护着一个指向可迭代对象的引用,因此迭代器会阻止垃圾回收程序回收可迭代对象。
迭代器可以指通用的迭代,也可以指接口,还可以指正式的迭代器类型。
自定义迭代器
与Iterator接口类似,任何实现Iterator 接口的对象都可以作为迭代器使用。
为了让一个可迭代对象能够创建多个迭代器,必须每创建一个迭代器就对应一个新计数器。为此,可以把计数器变量放到闭包里,然后通过闭包返回迭代器:
class Counter{
constructor(limit){
this.limit = limit;
}
[Symbol.iterator] (){
let count = 1;
limit = this.limit;
return {
next(){
if(count <= limit){
return{ done : false, value : count++ };
} else{
return{ done : true, value : undefiend };
}
}
};
}
}
let counter = new Counter(3);
for(let i of counter) { console.log(i); }
// 1
// 2
// 3
for(let i of counter) { console.log(i); }
// 1
// 2
// 3
Symbol.iterator 属性引用的工厂函数会返回相同的迭代器。因为每个迭代器都实现了 Iterator接口,所以它们可以用在任何期待可迭代对象的地方:
let arr = [3,1,4];
let iter = arr[Symbol.iterator]();
for(let item of arr) { console.log(item); }
// 3
// 1
// 4
for(let item of iter) { console.log(item); }
// 3
// 1
// 4
提前终止迭代器
可选的方法return()方法用于指定在迭代器提取关闭时执行的逻辑。
“关闭”迭代器可能的情况:
- for-of 循环通过break、continue、return或throw提前退出;
- 解构操作并未消费所有值。
return()方法必须返回一个有效的 IteratorResult 对象。简单情况下,可以只返回{ done:true },因为这个返回值只会用在生成器的上下文中。
内置语言结构在发现还有更多值可以迭代,但不会消费这些值时,会自动调用return()方法。
如果迭代器没有关闭,可以继续从上次离开的地方继续迭代,如数组的迭代器是不能关闭的。因为return()方法是可选的,并非所有迭代器都是可关闭的。
生成器【ES6新增】
拥有在一个函数块内暂停和恢复代码执行的能力。如可以自定义迭代器和实现协程。
生成器基础
形式是一个函数,函数名称前面加一个星号(*)表示其为生成器。只要是可以定义函数的地方,就可以定义生成器。
// 生成器函数声明
function* generatorFn(){}
// 生成器函数表达式
let generatorFn = function* (){}
// 作为对象字面量方法的生成器函数
let foo = {
* generatorFn(){}
}
// 作为类实例方法的生成器函数
class Foo{
* generatorFn(){}
}
// 作为类静态方法的生成器函数
class Bar {
static * generatorFn(){}
}
箭头函数不能用来定义生成器函数 标识函数的星号不受两侧空格影响。
调用生成器函数会产生一个生成器对象,一开始处于暂停执行(suspended)的状态。与迭代器类似,生成器对象也实现了Iterator接口,因此具有next()方法,调用这个方法会让生成器开始或恢复执行。
next()方法的返回值类似于迭代器,有done和value属性。函数体为空的生成器中间不会停留,调用一次next()就会让市场前期达到done:true状态。 value是生成器函数的返回值,默认值为undefined,可以通过生成器函数的返回值指定。
生成器函数只会在初次调用next()方法后开始执行。
通过yield 中断执行
yield可以让生成器停止和开始执行,是生成器最有用的地方。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行。
通过yield关键字退出的生成器函数会储再done:false状态;通过return关键字退出的生成器函数会处在done:true状态。
生成器函数内部的执行流程会针对每个生成器对象区分作用域,在一个生成器对象上调用next()不会影响其他生成器。
yield关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误:
function* validFn(){ // 有效
yield;
}
function* invalidFn(){ // 无效
function a(){
yield;
}
}
function* invalidFn2(){ // 无效
const b = () => {
yield;
}
}
1. 生成器对象作为可迭代对象
在需要自定义可迭代对象时,使用生成器对象会特别有用:
function* nTimes(n){
while(n--){
yield;
}
}
for(let _ of nTimes(3)){
console.log('log');
}
// foo 输出三次
2. 使用yield实现输入和输出
除了可以作为函数的中间返回语句,还可作为函数的中间参数使用。
第一次调用的next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数。
function* generatorFn(initial){
console.log(initial);
console.log(yield);
}
let g = generatorFn('foo');
g.next('jjk'); // foo 只是为了开始执行生成器函数
g.next('leo'); // leo
g.next('tjg'); // tjg
yield关键字可以同时用于输入和输出,并非只能用一次。
一个生成器函数,它会根据配置的值迭代相应次数并产生迭代的索引:
function* nTimes(n){
for(let i =0; i<n; ++i){
yield i;
}
}
for(let x of nTimes(3)){
console.log(x);
}
// 0
// 1
// 2
// 实现范围
function* range(start, end){
while(end > start){
yield start++;
}
}
for(let x of range(4,7)){
console.log(x); // 4 // 5 // 6
}
// 填充数组
function* fill(n){
while(n--){
yield 0;
}
}
console.log(Array.from(fill(8))); // [0, 0, 0, 0, 0, 0, 0, 0]
3. 产生可迭代对象
可使用星号增强yield的行为,可以将跟在它后面的可迭代对象序列化为一连串值。即让它能够迭代一个对象,从而一次产出一个值:
// 等价于 function* geneFn(){
// for(const x of [1,2,3]){
// yield x;
// }
//}
function* geneFn(){
yield* [1,2,3];
}
for(const x of geneFn()){
console.log(x); // 换行输出1,2,3
}
与生成器函数的星号类似,不受两侧空格的影响。
yield* 的值是关联迭代器返回done:true时的value 属性。对于普通迭代器来说,这个值是undefined。
function* printFn(){
console.log('iter value: ', yield* [1,2,3]);
}
for(const x of printFn()){
console.log('value:', x);
}
// value: 1
// value: 2
// value: 3
// iter value: undefined
对于生成器函数产生的迭代器来说,这个值就是生成器函数返回的值。
function* innerGeneFn(){
yield 'foo';
return 'xixi';
}
function* outerGeneFn(genObj){
console.log('iter value: ', yield* innerGeneFn());
}
for(const x of outerGeneFn()){
console.log('value', x);
}
// value: foo
// iter value: xixi
4. 使用yield* 实现递归算法
function* nTimes(n){
if(n>0){
yield* nTimes(n-1);
yield n-1;
}
}
for( const x of nTimes(3)){
console.log(x);
}
// 0
// 1
// 2
生成器函数会递归地减少计数器值,并实例化另一个生成器对象。
生成器作为默认迭代器
class Foo{
constructor(){
this.value = [1,2,3,];
}
* [Symbol.iterator](){
yield* this.value;
}
}
const f = new Foo();
for(const x of f){
console.log(x);
}
这里for-of循环调用了默认迭代器【恰好又是生成器函数】并产生了生成器对象。这个生成器对象是可迭代的,所以完全可以在迭代中使用。
提前终止生成器
与迭代器类似,“提前终止”的方法有,一个实现Iterator接口的对象一定有next()方法,还有一个可选的return()用于提前终止迭代器。除此之外,生成器还有throw()。
return()和throw()方法都可以用于强制生成器进入关闭状态。
1. return()
提供给return()方法的值,就是终止迭代器对象的值。
与迭代器不同,**所有生成器都有return(),只要通过它进入关闭状态,就无法恢复。**后续调用next()都会显示done:true状态,而提供的任何返回值都不会被存储或传播。
function* geneFn(){
for(const x of [1,2,3]){
yield x;
}
}
const g = geneFn();
console.log(g.next()); // {done : false, value : 1}
console.log(g.return(4)); // {done : true, value : 4}
console.log(g.next()); // {done : true, value : undefiend}
console.log(g.next()); // {done : true, value : undefiend}
for-of循环等内置语言结构会忽略状态为done:true的IteratorObject 内部返回的值:
function* geneFn(){
for (const x of [1,2,3]){
console.log(x);
}
}
const g = geneFn();
for(const x of g){
if(x>1){ g.return(4); }
console.log(x);
}
// 1
// 2
2. throw()
会在暂停的时候将一个提供的错误注入到生成器对象中,若错误未被处理,生成器就会关闭。
若生成器函数内部处理了这个错误,那么生成器不会关闭,而且还可以恢复执行,错误处理会跳过对应的yield:
function* geneFn(){
for(const x of [1,2,3]){
try{
yield x;
}catch(e) {}
}
}
const g = geneFn();
console.log(g.next(()); // {done : false, value: 1}
g.throw('foo');
console.log(g.next(()); // {done : false, value: 3}
throw()方法向生成器对象内部注入错误,这个错误会被yield关键字抛出,因为这个错误是在生成器的try/catch块中抛出的,所以仍然在生成器内部被捕获。由于yield抛出了那个错误,生成器就不会再产出值2。
如果生成器对象还没开始执行,那么调用throw()抛出的错误不会在函数内部被捕获,这相当于在函数块外部抛出了错误。