原文鏈接:http://wintc.top/site/article?postId=8
對(duì)于"數(shù)組遍歷"這個(gè)問(wèn)題,其實(shí)答案很寬泛,關(guān)鍵在于你能不能列舉出一定數(shù)量的方法以及描述它們之間的區(qū)別。本文即介紹一下數(shù)組的基本遍歷操作和高階函數(shù)。
一、數(shù)組基本遍歷
本部分介紹4種最常用的遍歷方式。
1.for...in
for...in其實(shí)是對(duì)象的遍歷方式,并不是數(shù)組專(zhuān)有,使用for...in將循環(huán)遍歷對(duì)象本身的所有可枚舉屬性,以及對(duì)象從其構(gòu)造函數(shù)原型中繼承的屬性,其遍歷順序與Object.keys()函數(shù)取到的列表一致。
該方法會(huì)遍歷數(shù)組中非數(shù)字下標(biāo)的元素,會(huì)忽略空元素:
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
這個(gè)方法遍歷數(shù)組是最坑的,它通常表現(xiàn)為有序,但是因?yàn)樗前凑諏?duì)象的枚舉順序來(lái)遍歷的,也就是規(guī)范沒(méi)有規(guī)定順序的,所以具體實(shí)現(xiàn)是由著瀏覽器來(lái)的。MDN文檔里也明確建議“不要依賴(lài)其遍歷順序”:

2.for...of
這個(gè)方法用于可迭代對(duì)象的迭代,用來(lái)遍歷數(shù)組是有序的,并且迭代的是數(shù)組的值。該方法不會(huì)遍歷非數(shù)字下標(biāo)的元素,同時(shí)不會(huì)忽略數(shù)組的空元素:
let list = [7, 5, 2, 3]
list[5] = 4
list[4] = 5
list[10] = 1
// 此時(shí)下標(biāo)6、7、8、9為空元素
list['a'] = 'a'
for (let value of list) {
console.log(value)
}
輸出:
> 7
> 5
> 2
> 3
> 5
> 4
> // 遍歷空元素
> // 遍歷空元素
> // 遍歷空元素
> // 遍歷空元素
> 1
3.取數(shù)組長(zhǎng)度進(jìn)行遍歷
該方法和方法2比較像,是有序的,不會(huì)忽略空元素。
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是數(shù)組的一個(gè)高階函數(shù),用法如下:
arr.forEach(callback[, thisArg])
參數(shù)說(shuō)明:
callback
為數(shù)組中每個(gè)元素執(zhí)行的函數(shù),該函數(shù)接收三個(gè)參數(shù):
- currentValue
數(shù)組中正在處理的當(dāng)前元素。
- index 可選
數(shù)組中正在處理的當(dāng)前元素的索引。
- array 可選
forEach() 方法正在操作的數(shù)組。
thisArg可選
可選參數(shù)。當(dāng)執(zhí)行回調(diào)函數(shù)時(shí)用作 this 的值(參考對(duì)象)。
forEach遍歷數(shù)組會(huì)按照數(shù)組下標(biāo)升序遍歷,并且會(huì)忽略空元素:
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
有一個(gè)很容易忽略的細(xì)節(jié),我們都應(yīng)該盡可能地避免在遍歷中取增刪數(shù)組的元素,否則會(huì)出現(xiàn)一些意外的情況,并且不同的遍歷方法還會(huì)有不同的表現(xiàn)。
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
可以看到,二者表現(xiàn)一致,遍歷到a的時(shí)候,把a刪除,則b會(huì)被跳過(guò),增加元素則略為不同。
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
咦,少了個(gè)'d'! 可以看到,其實(shí)forEach遍歷次數(shù)在一開(kāi)始就已確定,所以最后的'd'沒(méi)有輸出出來(lái),這是forEach和for遍歷數(shù)組的一個(gè)區(qū)別,另一個(gè)重要區(qū)別是forEach不可用break, continue, return等中斷循環(huán),而for則可以。
總之,在遍歷數(shù)組過(guò)程中,對(duì)數(shù)組的操作要非常小心,這一點(diǎn)python、js很相似,因?yàn)閮砷T(mén)語(yǔ)言中,對(duì)象/字典和數(shù)組都是引用,都為可變對(duì)象。
二、利用高階函數(shù)遍歷數(shù)組
上面介紹的4種算是比較標(biāo)準(zhǔn)的遍歷方式,不過(guò)JS中數(shù)組還有很多的高階函數(shù),這些函數(shù)其實(shí)都可以達(dá)到遍歷數(shù)組的目的,只不過(guò)每個(gè)函數(shù)的應(yīng)用場(chǎng)景不同,下面簡(jiǎn)單介紹一下。
1. map
map() 方法參數(shù)與forEach完全相同,二者區(qū)別僅僅在于map會(huì)將回調(diào)函數(shù)的返回值收集起來(lái)產(chǎn)生一個(gè)新數(shù)組。
比如將數(shù)組中每個(gè)元素的2倍輸出為一個(gè)新數(shù)組:
let list = [1, 2, 3, 4]
let result = list.map((value, idx) => value * 2)
console.log(result) // 輸出[2,4,6,8]
2.filter
filter() 參數(shù)與forEach完全一致,不過(guò)它的callback函數(shù)應(yīng)該返回一個(gè)真值或假值。filter() 方法創(chuàng)建一個(gè)新數(shù)組, 新數(shù)組包含所有使得callback返回值為真值(Truthy,與true有區(qū)別)的元素。
比如過(guò)濾數(shù)組中的偶數(shù):
let list = [1, 2, 3, 4]
let result = list.filter((value, idx) => value % 2 === 0)
console.log(result) // 輸出[2,4]
3. find/findIndex
find() 方法返回?cái)?shù)組中使callback返回值為T(mén)ruthy的第一個(gè)元素的值,沒(méi)有則返回undefined。使用非常簡(jiǎn)單,比如找出數(shù)組中第一個(gè)偶數(shù):
let list = ['1', '2', '3', '4']
let result = list.find(value => value % 2 === 0)
console.log(result) // 輸出 2
findIndex()方法與find方法很類(lèi)似,只不過(guò)findIndex返回使callback返回值為T(mén)ruthy的第一個(gè)元素的索引,沒(méi)有符合元素則返回-1。比如找出數(shù)組中第一個(gè)偶數(shù)的下標(biāo):
let list = [1, 2, 3, 4]
let result = list.findIndex(value => value % 2 === 0)
console.log(result) // 輸出 1
4.every/some
兩個(gè)函數(shù)接收參數(shù)都與以上函數(shù)相同,返回都是布爾值。every用于判斷是否數(shù)組中每一項(xiàng)都使得callback返回值為T(mén)ruthy,some用于判斷是否至少存在一項(xiàng)使得callback元素返回值為T(mén)ruthy。
let list = [1, 2, 3, 4]
// 判斷數(shù)組中是否每個(gè)元素小于10
let result = list.every(value => {
return value < 10
})
console.log(result) // 輸出true
// 判斷是否每個(gè)元素大于2
result = list.every(value => {
return value > 2
})
console.log(result) // 輸出false
// 判斷是數(shù)組中否存在1
result = list.some(value => {
return value === 1
})
console.log(result) // 輸出true
// 判斷數(shù)組中是否存在大于10的數(shù)
result = list.some(value => {
return value > 10
})
console.log(result) // 輸出false
5.reduce/reduceRight 累加器
參數(shù)與其它函數(shù)有所不同:
callback
執(zhí)行數(shù)組中每個(gè)值的函數(shù),包含四個(gè)參數(shù):
- accumulator
累計(jì)器累計(jì)回調(diào)的返回值; 它是上一次調(diào)用回調(diào)時(shí)返回的累積值,或initialValue(見(jiàn)于下方)。
- currentValue
數(shù)組中正在處理的元素。
- currentIndex 可選
數(shù)組中正在處理的當(dāng)前元素的索引。 如果提供了initialValue,則起始索引號(hào)為0,否則為1。
- array 可選
調(diào)用reduce()的數(shù)組
initialValue可選
作為第一次調(diào)用 callback函數(shù)時(shí)的第一個(gè)參數(shù)的值。 如果沒(méi)有提供初始值,則將使用數(shù)組中的第一個(gè)元素。 在沒(méi)有初始值的空數(shù)組上調(diào)用 reduce 將報(bào)錯(cuò)。
reduce() 方法對(duì)數(shù)組中的每個(gè)元素執(zhí)行一個(gè)由您提供的reducer函數(shù)(升序執(zhí)行),將其結(jié)果匯總為單個(gè)返回值,而reduceRight只是遍歷順序相反而已。
比如很常見(jiàn)的一個(gè)需求是,把一個(gè)如下結(jié)構(gòu)的list變成一個(gè)樹(shù)形結(jié)構(gòu),使用forEach和reduce可以輕松實(shí)現(xiàn)。
列表結(jié)構(gòu):
let list = [
{
id: 1,
parentId: ''
},
{
id: 2,
parentId: ''
},
{
id: 3,
parentId: 1
},
{
id: 4,
parentId: 2,
},
{
id: 5,
parentId: 3
},
{
id: 6,
parentId: 3
}
]
樹(shù)形結(jié)構(gòu):
[
{
"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實(shí)現(xiàn)list轉(zhuǎn)為樹(shù)形結(jié)構(gòu):
function listToTree(srcList) {
let result = []
// reduce收集所有節(jié)點(diǎn)信息存放在對(duì)象中,可以用forEach改寫(xiě),不過(guò)代碼會(huì)多幾行
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
}
以上即為本文圍繞數(shù)組遍歷介紹的數(shù)組基本操作。這些高階函數(shù)其實(shí)都可以用于數(shù)組遍歷(如果想強(qiáng)行遍歷的話,比如some的callback恒返回false),不過(guò)實(shí)際使用中應(yīng)該根據(jù)不同的需求選用不同的方法。
至此,面試中遇到“數(shù)組遍歷有多少種方法?”這種問(wèn)題,你可以回答“10種以上”了,畢竟,本文介紹了12種...