Contents
  1. 1. 从数组遍历谈起
    1. 1.1. forEach()
    2. 1.2. for-in
    3. 1.3. for-of
    4. 1.4. 对比
  2. 2. 更强大的for-of
    1. 2.1. for-of遍历原理
    2. 2.2. 遍历更多数据类型
    3. 2.3. 遍历普通对象
  3. 3. 迭代器对象的return()和throw()
  4. 4. 默认调用迭代器方法的场合
    1. 4.1. 解构赋值
    2. 4.2. 扩展运算符
    3. 4.3. 其他场合
  5. 5. 参考资料

从数组遍历谈起

最传统的数组遍历方法应该是以下这样:

1
2
3
for (var i = 0, len = myArr.length; i < len; i ++) {
console.log(myArr[i]);
}

这种方式看起来比较麻烦。

forEach()

后来ECMAScript5发布之后,我们可以使用forEach()方法来遍历数组:

1
2
3
myArr.forEach((value, key, array) => {
console.log(key + '-->' + value);
});

它可以读取数组的键,也可以读取数组的值

但是,它不可以在遍历过程中使用break、return来跳出循环,也不能使用return返回数据

for-in

我们也曾使用for-in来遍历数组:

1
2
3
for (var key in myArr) {
console.log(typeof key); // string
}

for-in循环用于遍历键(用字符串表示),而不是遍历值

它有以下3个明显的缺点:


  1. 数组的键(索引)为数组,而for-in循环遍历的到的是字符串’0’,’1’,’2’…
  2. for-in除了遍历数组键名外,还会遍历数组的自定义键名,比如myArr.name,则name会被遍历出来
  3. 一些情况下,for-in会以任意顺序遍历键名。

for-of

ES6为我们提供了一种新的循环语法:for-of

它的基本用法如下:

1
2
3
for (var value of myArr) {
console.log(value);
}

for-of避开了for-in循环的所有缺陷,它遍历的是数组的值

并且,与forEach()不同,它可以正常响应break、continue和return语句

1
2
3
4
5
6
7
8
// 只会输出数组中小于3的值
for (var value of myArr) {
if (value < 3) {
console.log(value);
} else {
break;
}
}

对比


  1. for-in遍历键(字符串表示),适合用来遍历对象属性
  2. for-of遍历值,并且可以遍历更多的数据类型(下面会讲到),并且可以配合break、continue、return等使用
  3. forEach()可以遍历键和值,但是不能使用break、continue等退出循环,也不能使用return返回数据

更强大的for-of

for-of循环其实主要为遍历可迭代对象(具有迭代器方法的一切对象)而生。

ES6规定,迭代器方法为[Symbol.iterator]()方法。

所以,只要某个对象实现了[Symbol.iterator]()的属性,那么这个对象就是可迭代对象,它就可以用for-of遍历。

在ES6中,有3类数据结构原生具有迭代器方法: 数组、某些类似数组的对象(NodeList、arguments、字符串)、Set和Map结构

for-of遍历原理


  1. for-of循环遍历某种数据结构时,会首先调用数据结构的访问器方法,即[Symbol.iterator]()方法,这个方法返回一个新的迭代器对象。迭代器对象是任何具有next()方法的对象,它不同于可迭代对象。

  2. for-of循环将重复调用迭代器对象中的next()方法,每次循环调用一次。

  3. 每次调用next()方法,都会返回一个包含valuedone两个属性的对象,其中value属性值为此次迭代的输出结果,而done属性值是一个布尔值,表示遍历是否结束。

以下代码定义了一个很简单的可迭代对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 一个简单的可迭代对象
var easyIterator = {
[Symbol.iterator] () {
return this;
},
next () {
return {
value: ++ this.num,
done: false
};
},
num: 0
};
// 使用for-of循环来遍历
for (let num of easyIterator) {
if (num < 10) {
console.log(num);
} else {
break;
}
}
// 输出1-9

遍历更多数据类型

除了遍历数组外,for-of还可以遍历更多的数据类型,包括Set和Map结构、类数据对象(比如arguments、DOM NodeList对象、字符串)、Generator对象

我们来看以下几个场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
/*
* 场景1: 遍历Set和Map结构,其原生具有迭代器方法
* 遍历的顺序是按照各个成员被添加进数据结构的顺序
* Set结构遍历时,返回的是一个值;但是Map结构则返回一个数组,包含key和value
*/
var colors = new Set(['red', 'red', 'green', 'blue']);
for (let color of colors) {
console.log(color);
}
// red
// green
// blue
var xwj = new Map();
xwj.set('name', 'xuwenjiang');
xwj.set('age', 20);
xwj.set('school', 'OUC');
for (let [key, value] of xwj) {
console.log(key + '-->' + value);
}
// name-->xuwenjiang
// age-->20
// school-->OUC
/*
* 场景2: 遍历计算生成的数据结构
* ES6的数组、Set、Map都部署了以下三个方法,调用后都返回迭代器对象
* entries():返回一个迭代器对象,用来遍历[key, value]
* keys(): 返回一个迭代器对象,用来遍历所有键名
* values(): 返回一个迭代器对象,用来遍历所有键值
*/
var arr = ['a', 'b', 'c'];
for (let item of arr.entries()) {
console.log(item);
}
// [0,'a']
// [1,'b']
// [2,'c']
/*
* 场景3: 遍历类似数组的对象,原生具有迭代器方法
* 类似数组的对象有:字符串、DOM NodeList、arguments
*/
// 1. 字符串
var myStr = 'xwj';
for (let c of myStr) {
console.log(c); // x w j
}
// 2. DOM NodeList对象
var balls = document.querySelectorAll('.ball');
for (let ball of balls) {
ball.classList.add('big');
}
//3. arguments对象
function sum () {
let sum = 0;
for (let x of arguments) {
sum += x;
}
return sum;
}
console.log(sum(1,2,3,4)); // 10
/*
* 场景4:当然,并非所有的类数组对象都具有迭代器方法
* 但是我们可以使用Array.from()方法将其转换为数组
* 类数组对象:拥有一个length属性和若干索引属性的任意对象
*/
var car = {
length: 2,
0: 'redColor',
1: 'bigSize'
};
console.log(Array.from(car)); // ["redColor", "bigSize"]
for(let x of Array.from(car)) {
console.log(x);
}

遍历普通对象

for-of不能直接遍历普通对象,只有实现了迭代器方法[Symbol.iterator]()才能使用。

这种情况下,我们可以使用for-in来遍历键名

或者采用Object.keys()将对象的键名生成一个数组,然后遍历这个数组,代码如下:

1
2
3
4
5
6
7
8
9
10
var xwj = {
name: 'xuwenjiang',
age: 20,
school: 'OUC'
};
for (let key of Object.keys(xwj)) {
console.log(key + '-->' + xwj[key]);
}

迭代器对象的return()和throw()

迭代器对象,除了具有next()方法,还有return()throw()方法。

其中next()方法是必须的,而return()和throw()方法时可选的

return()方法的使用场合是:如果for-of循环因为出错,或者因为break、continue语句而提前退出,就会调用return()方法。

throw()方法主要是配合Generator函数使用,一般的迭代器对象用不到这个方法。

默认调用迭代器方法的场合

有一些场合会默认调用可迭代对象的迭代器方法,即[Symbol.iterator()]方法,除了for-of循环,还有以下几个场合。

解构赋值

对数组和Set解构赋值时,会默认调用迭代器方法。以下代码证明了这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var mySet = new Set().add('a').add('b').add('c');
let [x, y, z] = mySet;
console.log(x + '-->' + y + '-->' + z); // a-->b-->c
// 假如我们加上以下代码
// 则输出结果为 'next-->next-->next'
mySet[Symbol.iterator] = () => {
return {
next () {
return {
value: 'next',
done: false
};
}
};
};

也就是说,我们可以将任何具有迭代器方法的数据结构,进行解构赋值

扩展运算符

扩展运算符也会默认调用迭代器方法:

1
2
3
4
5
6
7
// 例一
var str = 'xwj';
console.log(...str); // ['x','w','j']
// 例二
var arr = ['b', 'c']
console.log(['a', ...arr, 'd']); // ['a','b','c','d']

也就是说,我们可以将任何具有迭代器方法的数据结构,通过扩展运算符,将它转化为数组

其他场合

由于数组的遍历会调用迭代器方法,所以任何可以接收数组作为参数的场合,其实都调用了迭代器方法。下面是一些例子:


  1. Array.from()
  2. Map(),Set()
  3. Promise.all()
  4. Promise.race()

参考资料

Contents
  1. 1. 从数组遍历谈起
    1. 1.1. forEach()
    2. 1.2. for-in
    3. 1.3. for-of
    4. 1.4. 对比
  2. 2. 更强大的for-of
    1. 2.1. for-of遍历原理
    2. 2.2. 遍历更多数据类型
    3. 2.3. 遍历普通对象
  3. 3. 迭代器对象的return()和throw()
  4. 4. 默认调用迭代器方法的场合
    1. 4.1. 解构赋值
    2. 4.2. 扩展运算符
    3. 4.3. 其他场合
  5. 5. 参考资料