Iterator
JavaScript原有的表示“集合”的数据结构:
Array、Object、Map、Set
用户还可以组合他们,定义自己的数据结构。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是ES6创造了一种新的遍历命令for…of循环,Iterator接口主要供for…of消费。
遍历过程
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next都会返回value和done,value是当前遍历到的成员的值,done是是否遍历完成的布尔值。
数据结构的默认Iterator接口
一种数据结构只要部署了Iterator接口,我们就称这种数据结构是”可遍历的“(iterable)。
ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性
在ES6中,有三类数据结构原生具备Iterator接口:数组、某些类似数组的对象(字符串,NodeList)、Set和Map结构,
我们来看一下数组的:
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 'a', done: false }
console.log(iter.next()); // { value: 'b', done: false }
console.log(iter.next()); // { value: 'c', done: false }
console.log(iter.next()); // { value: undefined, done: true }
由此看来,这个接口最重要的就是这个next方法,这个方法应该可以根据遍历的情况返回当前的遍历值value和是否遍历完成的标志done。
那么只要我在一个对象的Symbol.iterator上部署了一个合乎规定的next方法,那这个对象就可以使用for…of遍历了。
我们来看一个例子,使用遍历器实现单链表结构的遍历:
//链表节点本身
function Node(value) {
this.value = value;
this.next = null;
}
//链表节点的遍历方法,定义在Symbol.iterator上
Node.prototype[Symbol.iterator] = function() {
//遍历方法必须返回一个遍历器对象
//这个对象唯一强制的条件就是具有一个符合要求的next方法
var iterator = {
next: next
};
var current = this;
//对于这个next方法,必须返回一个具有done属性和value属性的对象
function next() {
if (current) {
var value = current.value;
current = current.next;
return {
done: false,
value: value
};
} else {
return {
done: true
};
}
}
return iterator;
}
var one = new Node(1);
var two = new Node(2);
var three = new Node(3);
one.next = two;
two.next = three;
for (var i of one){
console.log(i);
}
// 1
// 2
// 3
对于类似数组的对象,也就是存在以0,1,2……为键,并有length属性的对象,直接使用数组的遍历器就行:
NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
[...document.querySelectorAll('div')] // 可以执行了
调用Iterator接口的场合
有一些场合会默认调用Iterator接口
解构赋值
let set = new Set().add('a').add('b').add('c');
let [first, ...rest] = set;
// first='a'; rest=['b','c'];
扩展运算符
只要某个数据结构部署了Iterator接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {
yield 1;
yield* [2,3,4];
yield 5;
};
var iterator = generator();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
for (var i of generator()) {
console.log(i);
}
任何接受数组为参数的地方
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。
Iterator接口与Generator函数
Symbol.iterator方法的最简单实现,还是使用Generator函数。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
遍历器对象的return(),throw()
遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。
return方法的使用场合是,如果for…of循环提前退出(通常是因为出错,或者有break语句或continue语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
var str = new String('abcdefghijklmn');
str[Symbol.iterator] = function() {
var index = 0;
var _length = this.length;
return {
next: function(){
if (index<_length)
return {value:str[index++]+'1',done:false};
else
return {done:true};
},
return: function() {
console.log('somthing wrong');
return { done: true };
}
}
}
for (var i of str) {
console.log(i);
if (i==='l1')
break;
}
这里FF测试不成功。
throw方法主要是配合Generator函数使用
for…of循环
一个数据结构只要部署了Symbol.iterator属性,就被视为具有iterator接口,就可以用for…of循环遍历它的成员。for…of循环内部调用的是数据结构的Symbol.iterator方法。
for…of循环可以使用的范围包括数组、Set和Map结构、某些类似数组的对象(比如arguments对象、DOM NodeList对象)、Generator对象,以及字符串。
遍历Set
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
遍历Map
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
计算生成的数据结构
有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6的数组、Set、Map都部署了以下三个方法,调用后都返回遍历器对象。
- entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于Set,键名与键值相同。Map结构的iterator接口,默认就是调用entries方法。
- keys() 返回一个遍历器对象,用来遍历所有的键名。
- values() 返回一个遍历器对象,用来遍历所有的键值。
这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。
类似数组的对象
字符串、DOM NodeList对象、arguments对象
对于字符串来说,for…of循环还有一个特点,就是会正确识别32位UTF-16字符。
并不是所有类似数组的对象都具有iterator接口。你可以像上面说的把Array的Symbol.iterator赋值过来。还有一个更简便的解决方法,就是使用Array.from方法将其转为数组。
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}