什么是迭代?
在软件开发领域当中,“迭代”的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。ES6当中新增了两个高级特性,能够更清晰、高效、方便地实现迭代。
在js中,计数循环就是一种最简单的迭代。
for(let i=0;i<=10;i++){
console.log(i);
}
循环是迭代机制的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
因为数组有已知的长度,且每个位置的每一项可以通过索引来获得,所以整个数组可以通过递增索引来遍历。但是有如下原因。通过这种循环来执行例程并不理想。
- 迭代之前需要事先知道如何使用数据结构。数组中的每一项都只能通过引用取得数组对象,然后再通过
[]操作符取得特定的索引位置上的项。这种情况并不适用于所有数据结构。 2.遍历顺序并不是数据结构固有的。通过递增索引来访问数据是特定于数组类型的方式,并不适用于其他具有隐式顺序的数据结构。
可迭代的内置对象有哪些
一般来说什么对象是可以迭代的对象,只要在原型链上有Symbol.iterator方法的,理论上都是可迭代对象。很多内置类型都是实现了,比如:
- 字符串
- 数组
- 映射
- 集合
- arguments对象
- NodeList 等DOM集合类型
比如我们可以自行实现看一下数组和对象,数组原型链上含有Symbol.iterator方法,而对象是没有的。也为什么说明数组可以用for of进行遍历(for of可以遍历可迭代的对象),但是对象不行。这也是我认为对象和数组最大的区别点之一。数组可进行迭代,但是对象不行。
console.log([][Symbol.iterator]);//ƒ values() { [native code] }
console.log({}[Symbol.iterator]);//undefined
//再看看其他类型的数据
console.log(1[Symbol.iterator]);//undefined
console.log(false[Symbol.iterator]);//undefined
console.log('1'[Symbol.iterator]);//ƒ [Symbol.iterator]() { [native code] }
console.log((new Map())[Symbol.iterator]);//ƒ entries() { [native code] }
console.log((new Set())[Symbol.iterator]);//ƒ values() { [native code] }
而接受可迭代对象的原生语言特性包括:
for of循环- 数组解构(
const {length}=[];) - 拓展操作符(
...) Array.form()- 创建集合
- 创建映射
Promise.all()接收由期约组成的可迭代对象Promise.race()接收由期约组成的可迭代对象yield*操作符
这些原生语言结构会在后台调用提供的可迭代对象的Symbol.iterator函数(又称迭代器工厂函数),从而生成一个迭代器。
迭代器协议
迭代器是一种一次性使用的对象,用于迭代与其关联的可迭代对象。迭代器API使用next()方法,在可迭代对象中遍历数据。每次成功调用next(),都会返回一个IteratorResult对象,其中包含迭代器返回的下一个值。若不调用next(),则无法知道迭代器的当前位置。
next()方法返回的迭代器对象IteratorResult包含两个属性:done和value。done是一个布尔值,表示是否还可以再次调用next()取得下一个值;value表示可迭代对象的下一个值(done为false),或者undefined(done为true)。done:true状态为“耗尽”。比如:
let arr=['foo','bar'];
//用迭代器工厂函数生成一个迭代器
let iter=arr[Symbol.iterator]();
console.log(iter);//ArrayIterator {}
//执行迭代
console.log(iter.next());//{value: 'foo', done: false}
console.log(iter.next());//{value: 'bar', done: false}
console.log(iter.next());//{value: undefined, done: true}
每个迭代器都表示对可迭代对象的一次性有序遍历。每个不同的迭代器实例之间没有相互联系。都可以单独的遍历迭代对象。而且迭代器不会与迭代对象的某个时刻的快照绑定,就算在迭代期间被修改,那么迭代器也会反映相应的变化。比如:
let arr=['foo','bar'];
let iter1=arr[Symbol.iterator]();
let iter2=arr[Symbol.iterator]();
//执行迭代
console.log(iter1.next());//{value: 'foo', done: false}
console.log(iter1.next());//{value: 'bar', done: false}
console.log(iter2.next());//{value: 'foo', done: false}
console.log(iter2.next());//{value: 'bar', done: false}
arr.push('baz');
console.log(iter1.next());//{value: 'baz', done: false}
console.log(iter2.next());//{value: 'baz', done: false}
提前终止迭代器
如果想提前终止迭代器,可以使用迭代器对象的return()方法,return()方法用于指定在迭代器提前关闭时执行的逻辑。执行迭代的结构在想让迭代器知道它不想遍历到可迭代对象耗尽时,就可以提前关闭迭代器。比如以下情况会调用迭代器的return()方法:
for-of循环通过break、contine、return、或throw提前退出;- 解构操作并未消费所有值。
return()方法必须返回一个有效的IteratorResult对象。简单情况可以只返回{done:true}。而且这个返回值只会用在生成器的上下文中。比如:
class Counter {
constructor(limit){
this.limit=limit;
}
[Symbol.iterator](){
let count=1;
let limit=this.limit;
return {
next(){
if(count<=limit){
return {done:false,value:count++};
}else {
return {done:true};
}
},
return(){
console.log('提前终止');
return {done:true};
}
}
}
}
let counter1=new Counter(5);
for(let i of counter1){
if(i>2){
break;//提前终止
}
console.log(i);
}
let counter2=new Counter(5);
let [a,b]=counter2;//提前终止
console.log([a,b]);
如果迭代器没有关闭,则还可以继续从上次离开的地方继续迭代。比如,数组的迭代器就是不能关闭的:
let a=[1,2,3,4,5];
let iter=a[Symbol.iterator]();
for(let i of iter){
console.log(i);
if(i>2){
break;
}
}//1,2,3
console.log(iter.return);//undefined
for(let i of iter){
console.log(i);
}//4,5
因为return()方法是可选的,所以并非所有迭代器都是可以关闭的。但是,需要明白一点的是,如果只是单纯的给不可关闭迭代器实例加一个return()方法,并不能让它变成可关闭的。因为调用return()不会强制迭代器进入关闭状态。但是如果你代码里面用了我们刚刚提到的提前终止迭代器的两种方法之一,它还是会调用return()方法。比如:
let a=[1,2,3,4,5];
let iter=a[Symbol.iterator]();
iter.return=function(){
console.log('提前终止');
return {done:true};
}
for(let i of iter){
console.log(i);
if(i>2){
break;//提前终止
}
}//1,2,3
for(let i of iter){
console.log(i);
}//4,5
生成器
生成器拥有在一个函数块内暂停和恢复代码执行的能力。可以用生成器自定义迭代器和实现协程。
生成器基础
生成器的形式是一个函数,函数名称前面加一个(*)表示它是一个生成器。注意:箭头函数不能用来定义生成器函数。
调用生成器函数会产生一个生成器对象。生成器对象一开始处于暂停状态。与迭代器类似,生成器对象也实现了Iterator接口,因此具有next()方法。调用这个方法会让生成器开始或恢复执行。但是要记住,迭代器对象是迭代器对象,生成器对象是生成器对象,二者不能混为一谈。个人理解是,生成器相当于把迭代器再进行封装了一遍,而且,生成器里面的迭代器是自引用。自引用也就是调用Symbol.iterator()是相当于返回的是本身这个实例对象。比如:
let a=[1,2,3,4,5];
let iter=a[Symbol.iterator]();
function* generatorFn() {};
let generatorObject=generatorFn();
console.log(iter);//Array Iterator {}
console.log(generatorObject);//generatorFn {<suspended>}
console.log(generatorObject.next());//{value: undefined, done: true}
//自引用
console.log(generatorObject===generatorObject[Symbol.iterator]());//true
通过yield中断执行
yield关键字可以让生成器停止和开始执行,也是生成器最有用的地方。生成器函数在遇到yield关键字之前会正常执行,但是遇到这个关键字之后,执行会停止,函数作用域的状态会被保留。停止执行的生成器函数只能通过在生成器对象上调用next()方法来恢复执行,而且生成器函数只有调用next()方法才会开始执行!比如:
function* generatorFn() {
console.log('第一阶段');
yield 1;
console.log('第二阶段');
yield 2;
};
let generatorObject=generatorFn();
console.log(generatorObject.next());//第一阶段 {value: 1, done: false}
console.log(generatorObject.next());//第二阶段 {value: 2, done: false}
console.log(generatorObject.next());//{value: undefined, done: true}
yield关键字只能在生成器函数内部使用,用在其他地方会抛出错误。类似函数的return关键字,yield关键字必须直接位于生成器函数定义中,出现在嵌套的非生成器函数中会抛出语法错误。
- 生成器对象作为可迭代对象
在生成器对象上显示调用
next()的用处并不大。其实,如果把生成器对象当成可迭代对象,那么使用起来会更方便一些:
function* generatorFn() {
yield 1;
yield 2;
yield 3;
}
for(let i of generatorFn()){
console.log(i);
}
//1
//2
//3
- 使用yield实现输入和输出
除了可以作为函数的中间返回语句使用,yield关键字还可以作为函数的中间参数使用。上一次让生成器函数暂停的yield关键字会接收到传给next()方法的第一个值。这里有个地方不太好理解———第一次调用next()传入的值不会被使用,因为这一次调用是为了开始执行生成器函数:
function* generatorFn(str) {
console.log(str);
const a=yield 1;
console.log(a);
const b=yield 2;
console.log(b);
}
let generatorObject=generatorFn('foo');
generatorObject.next('bar');//foo
generatorObject.next('baz');//baz
generatorObject.next('qux');//qux
yield关键字可以同时用于输入和输出,如下例所示
function* generatorFn(){
return yield 'foo';
}
let generatorObject=generatorFn();
console.log(generatorObject.next());//{value: 'foo', done: false}
console.log(generatorObject.next('bar'));//{value: 'bar', done: true}
3.产生可迭代对象
可以使用星号增强yield的行为,让它能够迭代一个可迭代的对象,从而一次产出一个值:
function* generatorFn(){
yield* [1,2,3];
}
let generatorObject=generatorFn();
console.log(generatorObject.next());//{value: 1, done: false}
console.log(generatorObject.next());//{value: 2, done: false}
console.log(generatorObject.next());//{value: 3, done: false}
console.log(generatorObject.next());//{value: undefined, done: true}
//let generatorObject2=generatorFn();
for(let i of generatorFn()){
console.log(i);
}
//1
//2
//3
提前终止生成器
与迭代器类似,生成器也支持“可关闭”的概念。一个实现Iterator接口的对象一定有next()方法,还有一个可选地return()方法用于提前终止迭代器。生成器对象除了有这两个方法,还有第三个方法:throw()。这两个方法都可以用于强制生成器进入关闭状态。
return()与迭代器不同,所有生成器对象都有return()方法,只要通过它进入关闭状态,就再也无法恢复了。后续调用next()会显示{done: true}状态,而提供的任何返回值都不会被存储或传播:
function* generatorFn(){
yield* [1,2,3];
}
const generatorObject=generatorFn();
console.log(generatorObject.next());//{value: 1, done: false}
console.log(generatorObject.return(4));//{value: 4, done: true}
console.log(generatorObject.next());//{value: undefined, done: true}
throw()throw()方法会在暂停的时候将一个提供的错误注入到生成器对象。如果错误未被处理,生成器就会关闭:
function* generatorFn(){
for(let x of [1,2,3]){
yield x;
}
}
const g=generatorFn();
console.log(g);//generatorFn {<suspended>}
console.log(g.next());//{value: 1, done: false}
try{
g.throw('foo');
} catch(err) {
console.log(err);//foo
}
console.log(g.next());//{value: undefined, done: true}
console.log(g);//generatorFn {<closed>}
如果生成器函数内部里面处理了这个错误,那么生成器就不会关闭,还可以恢复执行。错误处理会跳过对应的yield。比如
function* generatorFn(){
for(let x of [1,2,3]){
try {
yield x;
}catch (e){
}
}
}
const g=generatorFn();
console.log(g);//generatorFn {<suspended>}
console.log(g.next());//{value: 1, done: false}
try{
g.throw('foo');
} catch(err) {
console.log(err);//这里的错误不会被捕获到,而是会被生成器函数里面的catch捕获到
}
console.log(g.next());//{value: 3, done: false} 跳过了2的情况
console.log(g);//generatorFn {<suspended>}
如果生成器对象还有调用过next()方法,就直接调用了throw()方法,那么错误就不会被生成器函数内部的trycatch捕获到,因为没有调用next()方法,相当于生成器对象还没开始执行。