源于一次面试,一起面试的同事问面试者的一个问题:数组遍历有哪些方式?想来数组操作是平时开发中的常用技能,面试者吞吞吐吐大概就说出了两种方式吧,最后就淘汰掉啦(面试者是个很认真的妹纸,面试都在简单做一些笔记,不过基础确实有些困难~)。
对于"数组遍历"这个问题,其实答案很宽泛,关键在于你能不能列举出一定数量的方法以及描述它们之间的区别。本文即介绍一下数组的基本遍历操作和高阶函数。
一、数组基本遍历
本部分介绍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 文档里也明确建议“不要依赖其遍历顺序”:
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 中数组还有很多的高阶函数,这些函数其实都可以达到遍历数组的目的,只不过每个函数的应用场景不同,下面简单介绍一下。
1. 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
}
以上即为本文围绕数组遍历介绍的数组基本操作。这些高阶函数其实都可以用于数组遍历(如果想强行遍历的话,比如 some 的 callback 恒返回 false),不过实际使用中应该根据不同的需求选用不同的方法。
至此,面试中遇到“数组遍历有多少种方法?”这种问题,你可以回答“10 种以上”了,毕竟,本文介绍了 12 种 …
最后,JS 其实是一门特别愚蠢的语言,有时候你交给它的事情,它不会办不说,竟然还会骂人!不信?控制台输入下面的算式试试:
(![]+{})[-~!+[]^-~[]]+([]+{})[-~!![]]
Just for fun. 别太认真~,~
本文原创,首发于我的个人网站:http://wintc.top/site/article?postId=8,转载请注明出处。