在ES6中,添加了Set和Map结构(ES6学习笔记(五)Set结构和Map结构),加上之前的数组和对象,一共有四种可以表示“集合”的数据结构,而通过组合使用它们可以定义自己的数据结构。为了处理不同的数据结构,需要一个统一的机制,而Iterator接口就是这样的机制。
Iterator接口的作用和遍历过程
作用:Iterator接口用于为不同的数据结构提供统一的接口,使得数据结构的成员能按一定的次序遍历,部署了Iterator接口的数据结构可以使用for...of来遍历。即Iterator接口就是为了实现不同数据结构的遍历,而遍历又是有序的,所以没有部署Iterator接口的数据结构成员在部署Iterator接口后就从无序变为线性有序了。
遍历过程:Iterator接口的遍历过程是通过指针来进行的,它首先将指针指向第一个位置,然后通过遍历器中的next方法来将指针移动到下一个位置(部署Iterator接口时一定要部署next方法),通过done(布尔值)来判断是否已经遍历到尾部,当done为true时,遍历结束。
下面是一个遍历器的模拟方法
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
makeIterator方法用来模拟一个遍历器,一开始将指针指向下标为0的位置,每次调用next方法移动到下一个位置前判断当前是否到了数组的尾部,如果还没有,返回value为下一个值,done为false的对象,如果到了尾部,返回value为undefined,done为true的对象。
这里可能会有提出输出最后一个值时,使done变为true结束的想法,因为这样就不会多输出一个{value:undefined, done:true}了,但我们可以通过下面的for...of循环的等价for形式来理解为什么输出最后一个值时done还要为false。
for(var v,res;(res=it.next())&&!res.done;){ // it为迭代器
v=res.value;
console.log(v);
}
如果输出最后一个值时done为true,那么该值会被抛弃,不会进入循环体中,所以应该在输出最后一个值后再把done变为true。
默认的Iterator接口
默认的Iterator接口部署在Symbol.Iterator属性,只要结构有这个属性,就是可以遍历的,而这个属性本身是一个方法,如果调用该方法,会返回一个遍历器,而可以通过该遍历器的next方法来遍历结构中的每个值,知道done为true
ES6中具备Iterator接口的数据结构有:Array,Map,Set,String,TypeArray,arguments对象,NodeList对象
var arr=[1,2,3];
var i=arr[Symbol.iterator]();
i.next();//{value: 1, done: false}
i.next();//{value: 2, done: false}
i.next();//{value: 3, done: false}
i.next();//{value: undefined, done: true}
var str='hello';
var i=str[Symbol.iterator]();
i.next();//{value: "h", done: false}
i.next();//{value: "e", done: false}
i.next();//{value: "l", done: false}
i.next();//{value: "l", done: false}
i.next();//{value: "o", done: false}
i.next();//{value: undefined, done: true}
这里的'hello'实际上被强制转换成String对象封装对象,通过获取其迭代器遍历其字符。
部署Iterator接口
要遍历一个没有Iterator接口的数据结构,需要在Symbol.iterator属性上部署遍历器方法(或者在原型链上部署该方法)
class RangeIterator {
constructor(start, stop) {//初始化遍历的起点和终点
this.value = start;
this.stop = stop;
}
[Symbol.iterator]() {
return this;
}//返回该遍历器
next() {
var value = this.value;//取得当前值
if (value < this.stop) {//判断当前是否走到了结尾
this.value++;
return {done: false, value: value};
}
return {done: true, value: undefined};
}
}
function range(start, stop) {
return new RangeIterator(start, stop);
}
for (var value of range(0, 3)) {
console.log(value); // 0, 1, 2
}
上面的代码部署了一个iterator接口,通过其Symbol.iterator方法来u后去其遍历器对象,然后用for...of来遍历该结构
function Obj(value) {
this.value = value;
this.next = null;//一开始的指针指向null
}
Obj.prototype[Symbol.iterator] = function() {
var iterator = { next: next };
var current = this;
function next() {
if (current) {//如果没有知道next的话,current为null,所以这里用于判断是否到了遍历的最后
var value = current.value;
current = current.next;//移到下一个位置
return { done: false, value: value };
} else {
return { done: true };
}
}
return iterator;
}
var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i); // 1, 2, 3
}
上面的代码声明了一个Obj方法,通过在原型链上的Symbol.iterator上部署iterator接口,在实例上调用Symbol.iterator方法时返回遍历器对象。
对于类似数组,可以直接将其Symbol.iterator方法改为直接引用数组的Iterator接口
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者NodeList.prototype[Symbol.iterator] = [][Symbol.iterator]
普通对象部署数组的Symbol.iterator方法,并无效果
Object.prototype[Symbol.iterator]=Array.prototype[Symbol.iterator];
let o=new Object({a:1,b:2});
for (let i of o)
console.log(i);//undefined
上面的代码虽然给Object对象部署了数组的Symbol.iterator方法,但是用for...of遍历时返回了undefined。
给Symbol.iterator部署方法时要注意,返回的必须是一个遍历器,如果返回了其他结构或者没有返回,浏览器会报错。
let o={a:1}
o[Symbol.iterator]=()=>1;
for (let i of o)
console.log(i);
自动调用iterator接口
在部署iterator接口后的数据结构和有默认iterator接口的数据在使用中,其实有些地方会自动调用到iterator接口
1.解构赋值
在对数组和Set结构进行解构赋值时,会默认调用Symbol.Iterator方法,我的理解是,数组和Set结构要进行解构赋值,需要获取里面每一个值,需要使用遍历操作,所以会默认调用Symbol.Iterator方法。
2.扩展运算符
扩展运算符是通过iterator接口遍历数据结构,将里面的成员取出来后以逗号分隔
3.yield*
Yield*后面跟一个可遍历的结构,会调用该结构的遍历器接口
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
4.数组的遍历
在遍历数组的时候实际上就是调用了iterator接口,所以任何接受数组为参数的场合都默认调用iterator接口,下面几个例子都会调用iterator接口
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()
- Promise.all()
- Promise.race()
5.Set和Map结构的遍历方法
Set和Map结构在调用keys,values,entries方法时会返回一个遍历器对象。(SetIterator和MapIterator)
遍历器对象的其他方法
遍历器除了next方法外,还有return方法和throw方法,如果要自己部署iterator接口,next方法是必要的,但是return方法和throw方法可以不要。
return方法是在循环中途要退出遍历的时候调用。
throw方法是用于配合Generator函数
JavaScript的几种遍历方法
for循环
for (let i=0;i<arr.length;i++)
console.log(i);
for循环写起来较其他遍历方法会繁琐一点,需要声明一个用来遍历的变量,还需要写遍历结束条件以及步长。
forEach
arr.forEach(i=>console.log(i));
forEach虽然在遍历时比较方便,但不能在中途跳出遍历,break语句和return语句都无效
for...in
for (let i in arr)
console.log(i);
for..in会遍历数组的键名,但因为键名是字符串,所以只能返回字符串,所以for...in并不适合用于遍历数组,而适合用于遍历对象
for...of
for (let i of arr)
console.log(i);
for...of有和for...in一样简洁的写法,而且没有for...in的缺点,同时也可以使用break语句和return语句停止遍历。实际上for...of是在迭代中自动调用Symbol.iterator函数来构建一个迭代器,并自动调用next(),且不向next()传入任何值,在接收到done:true之后停止。
for...of可遍历的类型有:数组,Map,Set,Generator,字符串,类数组如参数对象arguments
自定义迭代器例子
(以下例子出自《你不知道的JavaScript 下卷》)
无限斐波拉契序列
var Fib = {
[Symbol.iterator]() {
var n1 = 1,
n2 = 1; // 初始数字,即序列的1,2位
return {
//使迭代器变为iterable,通过返回this
[Symbol.iterator]() { return this; },
next() {
var current = n2;
n2 = n1;
n1 = n1 + current; // 相加得到下一个数,再依次传递给n2保存,再下一次循环通过current返回
return { value: current, done: false };
},
return (v) {
console.log("Fibonacci sequence abandoned");
return { value: v, done: true }; // done变为true,结束迭代
}
}
}
}
for (var v of Fib) {
console.log(v);
if (v > 100) break;
}
// 1 1 2 3 5 8 13 21 34 55 89 144
// Fibonacci sequence abandoned
如果没有使用break跳出循环,这个循环会一直持续下去。
执行函数数组中的函数
var tasks = {
[Symbol.iterator]() {
var steps = this.actions.slice(); // 获取actions中的方法名数组
return {
// 使迭代器称为iterator
[Symbol.iterator]() { return this; },
next(...args) {
if (steps.length > 0) {
let res = steps.shift(...args); // 使用next传递过来的参数执行函数
return { value: res, done: false };
} else {
return { done: true };
}
},
return (v) {
steps.length = 0;
return { value: v, done: true };
}
}
},
actions: [] // 存储方法的数组
}
参考自阮一峰的《ECMAScript6入门》
Kyle Simpson的《你不知道的JavaScript 中卷》,《你不知道的JavaScript 下卷》
ES6学习笔记目录(持续更新中)