源於一次面試,一塊兒面試的同事問面試者的一個問題:數組遍歷有哪些方式?想來數組操做是平時開發中的經常使用技能,面試者吞吞吐吐大概就說出了兩種方式吧,最後就淘汰掉啦(面試者是個很認真的妹紙,面試都在簡單作一些筆記,不過基礎確實有些困難~)。node
對於"數組遍歷"這個問題,其實答案很寬泛,關鍵在於你能不能列舉出必定數量的方法以及描述它們之間的區別。本文即介紹一下數組的基本遍歷操做和高階函數。python
本部分介紹4種最經常使用的遍歷方式。面試
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文檔裏也明確建議「不要依賴其遍歷順序」:
post
這個方法用於可迭代對象的迭代,用來遍歷數組是有序的,而且迭代的是數組的值。該方法不會遍歷非數字下標的元素,同時不會忽略數組的空元素:網站
let list = [7, 5, 2, 3] list[5] = 4 list[4] = 5 list[10] = 1 // 此時下標六、七、八、9爲空元素 list['a'] = 'a' for (let value of list) { console.log(value) }
輸出:this
> 7 > 5 > 2 > 3 > 5 > 4 > // 遍歷空元素 > // 遍歷空元素 > // 遍歷空元素 > // 遍歷空元素 > 1
該方法和方法2比較像,是有序的,不會忽略空元素。spa
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
forEach是數組的一個高階函數,用法以下:
arr.forEach(callback[, thisArg])
參數說明:
callback
爲數組中每一個元素執行的函數,該函數接收三個參數:
數組中正在處理的當前元素。
數組中正在處理的當前元素的索引。
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遍歷中刪除元素:
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遍歷中增長元素:
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() 方法參數與forEach徹底相同,兩者區別僅僅在於map會將回調函數的返回值收集起來產生一個新數組。
好比將數組中每一個元素的2倍輸出爲一個新數組:
let list = [1, 2, 3, 4] let result = list.map((value, idx) => value * 2) console.log(result) // 輸出[2,4,6,8]
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]
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
兩個函數接收參數都與以上函數相同,返回都是布爾值。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
參數與其它函數有所不一樣:
callback
執行數組中每一個值的函數,包含四個參數:
累計器累計回調的返回值; 它是上一次調用回調時返回的累積值,或initialValue(見於下方)。
數組中正在處理的元素。
數組中正在處理的當前元素的索引。 若是提供了initialValue,則起始索引號爲0,不然爲1。
調用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,轉載請註明出處。