JavaScript原有的四种表示'集合'的数据结构,Object、Array、Set、Map。
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署了Iterator接口,就可以完成遍历操作。
Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of
循环,Iterator 接口主要供for...of消费。
Iterator 的遍历过程是这样的。
- 创建一个指针对象,指向当前数据结构的起始位置。遍历器对象本质上就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
模拟next方法返回值的例子
let it = makeIterator(['a','b']);
it.next();// { value: "a", done: false }
it.next();// { value: "b", done: false }
it.next();// { value: undefined, done: true }
fucntion makeIterator(array){
let nextIndex = 0;
return {
next : function(){
return nextInedx < array.length ?
{value : array[nextIndex++], done : false} :
{value : undefined, done : true};
}
}
}
默认Iterator接口
Iterator接口的目的就是为所有数据结构,提供了一种统一的访问机制,即for...of
循环。当使用for...of
循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内。
const obj = {
[Symbol.iterator] : function(){
return {
next : function(){
return {
value : 1,
done : true
}
}
}
}
};
上面代码中,对象obj是可遍历的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有value和done两个属性.
ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。
- Array
- Map
- Set
- String
- TypedArray
- 函数的arguments对象
- NodeList对象
这些数据结构具备原生的Iterator接口。
数组的Symbol.iterator属性。
let arr = ['a','b','c'];
let iter = arr[Symbol.iterator]();
iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arr的Symbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
要被for...of
循环调用Iterator接口,就必须在Symbol.iterator的属性上部署属性遍历器生成方法。
class RangeIterator {
constructor(start, stop){
this.value = start;
this.stop = stop;
}
[Symbol.iterator]{return this;}
next(){
let 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(let v of range(0,3)) {
console.log(value); // 0 1 2
}
下面是一个通过遍历器实现指针结构的例子
function Obj(value){
this.value = value;
this.next = null;
}
Obj.prototype[Symbol.iterator] = function(){
let iterator = {next : next}; //遍历器对象
let current = this;
//返回的遍历器对象必须要有next函数
function next(){
console.log(current);
if(current){
let value = current.value;
current = current.next;
return {done : false, value : value} //return的对象格式
}else{
return {done : true};
}
}
return iterator;
}
let one = new Obj(1);
let two = new Obj(2);
let three = new Obj(3);
one.next = two;
two.next = three;
for(let i of one){
console.log(i);
}
//Obj {value: 1, next: Obj}
//1
//Obj {value: 2, next: Obj}
//2
//Obj {value: 1, next: null}
//3
上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时({value : value,done : boolean}),将内部指针(上面的create)指向下一个实例。
注意:每次都是调用被遍历对象的next方法
对于类数组的对象(存在数字类键名和length属性),部署Iterator接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的Iterator接口。
let likeArray = {
0 : 'a',
1 : 'b',
2 : 'c',
length : 3,
[Symbol.iterator] : Array.prototype[Symbol.iterator]//或者[][Symbol.iterator]
};
for(let item of iterable){
console.log(item) //'a','b','c'
}
普通对象部署数组的Symbol.iterator方法,并无效果。
Object.prototype[Symbol.iterator] = () => {
let index = 0;
console.log(this === window); //true
let keys = Object.keys(this);
let length = keys.length;
let self = this;
return {next : next};
function next(){
if(index++ < length){
return {
value : self[keys[index]],
done : false,
};
}else{
return {
done : true
};
}
}
}
调用Iterator接口的场合
- 解构赋值
let set = new Set().add('a').add('b').add('c');
let [x,y] = set;
//x ='a' y = 'b'
let [first, ...rest] = set;
//first = 'a'; rest = ['b','c'];
- 扩展运算符
// 例一
var str = 'hello';
[...str] // ['h','e','l','l','o']
// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']
- 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 }
for ... of
Array.from()
Map()、Set()...
for...of循环
一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。
for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、Generator 对象,以及字符串。
JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。
var arr = ['a', 'b', 'c', 'd'];
for (let a in arr) {
console.log(a); // 0 1 2 3
}
for (let a of arr) {
console.log(a); // a b c d
}
如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法
for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。
let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
console.log(i); // "0", "1", "2", "foo"
}
for (let i of arr) {
console.log(i); // "3", "5", "7"
}
let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]
for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 报错
for (let x of arrayLike) {
console.log(x);
}
// 正确
for (let x of Array.from(arrayLike)) {
console.log(x);
}
对于普通对象
对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。
let es6 = {
edition: 6,
committee: "TC39",
standard: "ECMA-262"
};
for (let e in es6) {
console.log(e);
}
// edition
// committee
// standard
for (let e of es6) {
console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function
一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}
另一个方法是使用 Generator 函数将对象重新包装一下。
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]];
}
}
for (let [key, value] of entries(obj)) {
console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3
与其他遍历语法的比较
数组可以用for循环和forEach。
但是forEach无法中途跳出forEach循环,break,return都不奏效
for...in
循环可以遍历数组的键名。但是键为数字的字符串表示"0"
for...in循环有几个缺点。
数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
某些情况下,for...in循环会以任意顺序遍历键名。
总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of:
- 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
- 不同于forEach方法,它可以与break、continue和return配合使用。
- 提供了遍历所有数据结构的统一操作接口。
for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}