一、数组基本遍历
本部分介绍4种最常用的遍历方式。
1.for…in
for…in 其实是对象的遍历方式,并不是数组专有,使用 for…in 将循环遍历对象本身的所有可枚举属性,以及对象从其构造函数原型中继承的属性,其遍历顺序与 Object.keys()函数取到的列表一致。
该方法会遍历数组中非数字下标的元素,会忽略空元素:
let list = [7, 5, 2, 3]
list[10] = 1
list[‘a’] = 1
console.log(JSON.stringify(Object.keys(list)))
for (let key in list) {
console.log(key, list[key])
}
输出:
[“0”,”1”,”2”,”3”,”10”,”a”]
0, 7
1, 5
2, 2
3, 3
10, 1
a, 1
这个方法遍历数组是最坑的,它通常表现为有序,但是因为它是按照对象的枚举顺序来遍历的,也就是规范没有规定顺序的,所以具体实现是由着浏览器来的。MDN 文档里也明确建议“不要依赖其遍历顺序”:
image.png
2.for…of
这个方法用于可迭代对象的迭代,用来遍历数组是有序的,并且迭代的是数组的值。该方法不会遍历非数字下标的元素,同时不会忽略数组的空元素:
let list = [7, 5, 2, 3]
list[5] = 4
list[4] = 5
list[10] = 1
// 此时下标 6、7、8、9 为空元素
list[‘a’] = ‘a’
for (let value of list) {
console.log(value)
}
输出:
7
5
2
3
5
4
// 遍历空元素
// 遍历空元素
// 遍历空元素
// 遍历空元素
1 3.取数组长度进行遍历
该方法和方法 2 比较像,是有序的,不会忽略空元素。
let list = [‘a’, ‘b’, ‘c’, ‘d’]
list[4] = ‘e’
list[10] = ‘z’
list[‘a’] = 0
for (let idx = 0; idx < list.length; idx++) {
console.log(idx, list[idx])
}
输出:
0, a
1, b
2, c
3, d
4, e
5, //空元素
6,
7,
8,
9,
10, z
4.forEach 遍历
forEach 是数组的一个高阶函数,用法如下:
arr.forEach(callback[, thisArg])
参数说明:
callback
为数组中每个元素执行的函数,该函数接收三个参数:
currentValue
数组中正在处理的当前元素。
index 可选
数组中正在处理的当前元素的索引。
array 可选
forEach() 方法正在操作的数组。
thisArg 可选
可选参数。当执行回调函数时用作 this 的值(参考对象)。
forEach 遍历数组会按照数组下标升序遍历,并且会忽略空元素:
let list = [‘a’, ‘b’, ‘c’, ‘d’]
list[4] = ‘e’
list[10] = ‘z’
list[‘a’] = 0
list.forEach((value, key, list) => {
console.log(key, value)
})
输出:
0, a
1, b
2, c
3, d
4, e
10, z
有一个很容易忽略的细节,我们都应该尽可能地避免在遍历中取增删数组的元素,否则会出现一些意外的情况,并且不同的遍历方法还会有不同的表现。
for…of 和 forEach 遍历中删除元素
比如 for…of 遍历中删除元素:
let list = [‘a’, ‘b’, ‘c’, ‘d’]
for (let item of list) {
if (item === ‘a’) {
list.splice(0, 1)
}
console.log(item)
}
输出:
a
c
d
forEach 遍历中删除元素:
let list = [‘a’, ‘b’, ‘c’, ‘d’]
list.forEach((item, idx) => {
if (item === ‘a’) {
list.splice(0, 1)
}
console.log(item)
})
输出:
a
c
d
可以看到,二者表现一致,遍历到a的时候,把a删除,则b会被跳过,增加元素则略为不同。
for…of 和 forEach 遍历中增加元素
for…of 遍历中增加元素:
let list = [‘a’, ‘b’, ‘c’, ‘d’]
for (let item of list) {
if (item === ‘a’) {
list.splice(1, 0, ‘e’)
}
console.log(item)
}
输出:
a
e
b
c
d
forEach 遍历中增加元素:
let list = [‘a’, ‘b’, ‘c’, ‘d’]
list.forEach((item, idx) => {
if (item === ‘a’) {
list.splice(1, 0, ‘e’)
}
console.log(item)
})
输出:
a
e
b
c
咦,少了个’d’! 可以看到,其实 forEach 遍历次数在一开始就已确定,所以最后的’d’没有输出出来,这是 forEach 和 for 遍历数组的一个区别,另一个重要区别是 forEach 不可用 break, continue, return 等中断循环,而 for 则可以。
总之,在遍历数组过程中,对数组的操作要非常小心,这一点 python、js 很相似,因为两门语言中,对象/字典和数组都是引用,都为可变对象。
二、利用高阶函数遍历数组
上面介绍的4种算是比较标准的遍历方式,不过 JS 中数组还有很多的高阶函数,这些函数其实都可以达到遍历数组的目的,只不过每个函数的应用场景不同,下面简单介绍一下。
- map
map() 方法参数与 forEach 完全相同,二者区别仅仅在于 map 会将回调函数的返回值收集起来产生一个新数组。
比如将数组中每个元素的 2 倍输出为一个新数组:
let list = [1, 2, 3, 4]
let result = list.map((value, idx) => value * 2)
console.log(result) // 输出[2,4,6,8]
2.filter
filter() 参数与 forEach 完全一致,不过它的 callback 函数应该返回一个真值或假值。filter() 方法创建一个新数组, 新数组包含所有使得 callback 返回值为真值(Truthy,与 true 有区别)的元素。
比如过滤数组中的偶数:
let list = [1, 2, 3, 4]
let result = list.filter((value, idx) => value % 2 === 0)
console.log(result) // 输出[2,4] 3. find/findIndex
find() 方法返回数组中使 callback 返回值为 Truthy 的第一个元素的值,没有则返回 undefined。使用非常简单,比如找出数组中第一个偶数:
let list = [‘1’, ‘2’, ‘3’, ‘4’]
let result = list.find(value => value % 2 === 0)
console.log(result) // 输出 2
findIndex()方法与 find 方法很类似,只不过 findIndex 返回使 callback 返回值为 Truthy 的第一个元素的索引,没有符合元素则返回-1。比如找出数组中第一个偶数的下标:
let list = [1, 2, 3, 4]
let result = list.findIndex(value => value % 2 === 0)
console.log(result) // 输出 1
4.every/some
两个函数接收参数都与以上函数相同,返回都是布尔值。every 用于判断是否数组中每一项都使得 callback 返回值为 Truthy,some 用于判断是否至少存在一项使得 callback 元素返回值为 Truthy。
let list = [1, 2, 3, 4]
// 判断数组中是否每个元素小于 10
let result = list.every(value => {
return value < 10
})
console.log(result) // 输出 true
// 判断是否每个元素大于 2
result = list.every(value => {
return value > 2
})
console.log(result) // 输出 false
// 判断是数组中否存在 1
result = list.some(value => {
return value === 1
})
console.log(result) // 输出 true
// 判断数组中是否存在大于 10 的数
result = list.some(value => {
return value > 10
})
console.log(result) // 输出 false
5.reduce/reduceRight 累加器
参数与其它函数有所不同:
callback
执行数组中每个值的函数,包含四个参数:
accumulator
累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 initialValue(见于下方)。
currentValue
数组中正在处理的元素。
currentIndex 可选
数组中正在处理的当前元素的索引。 如果提供了 initialValue,则起始索引号为 0,否则为 1。
array 可选
调用 reduce()的数组
initialValue 可选
作为第一次调用 callback 函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
reduce() 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值,而 reduceRight 只是遍历顺序相反而已。
比如很常见的一个需求是,把一个如下结构的 list 变成一个树形结构,使用 forEach 和 reduce 可以轻松实现。
列表结构:
let list = [
{
id: 1,
parentId: ‘’
},
{
id: 2,
parentId: ‘’
},
{
id: 3,
parentId: 1
},
{
id: 4,
parentId: 2,
},
{
id: 5,
parentId: 3
},
{
id: 6,
parentId: 3
}
]
树形结构:
[
{
“id”:1,
“parentId”:””,
“children”:[
{
“id”:3,
“parentId”:1,
“children”:[
{
“id”:5,
“parentId”:3
},
{
“id”:6,
“parentId”:3
}
]
}
]
},
{
“id”:2,
“parentId”:””,
“children”:[
{
“id”:4,
“parentId”:2
}
]
}
]
利用 reduce 和 forEach 实现 list 转为树形结构:
function listToTree(srcList) {
let result = []
// reduce 收集所有节点信息存放在对象中,可以用 forEach 改写,不过代码会多几行
let nodeInfo = list.reduce((data, node) => (data[node.id] = node, data), {})
// forEach 给所有元素找妈妈
srcList.forEach(node => {
if (!node.parentId) {
result.push(node)
return
}
let parent = nodeInfo[node.parentId]
parent.children = parent.children || []
parent.children.push(node)
})
return result
}

