首先作一個粗體聲明:循環常常是無用的,而且使得代碼很難閱讀。 當談到迭代一個數組的時候,不管你想去查找元素,排序或者任何其餘的事,都有可能存在一個數組的方法供你使用。html
然而,儘管它們有用,但其中一些仍然不被人瞭解。我會努力爲你展現一些有用的方法。把這篇文章當作對 JavaScript 數組方法的指引吧。node
注意: 在開始以前,不得不瞭解一件事:我比較偏心函數式編程。因此我傾向於使用的方法不會直接改變原來的數組。這種方法,我避免了反作用。我不是說不該該改變數組,但至少要了解那些方法會改變,那些會有反作用。反作用致使不想要的改變,而不想要的改變帶來bugs!react
瞭解到這裏,咱們能夠開始正文了。編程
當跟數組打交道時,有四件事你應該清楚:map
,filter
,reduce
和 展開操做符。它們富有力量。數組
你能夠在不少種狀況下使用它。基本地,每次你須要修改數組的元素時,考慮使用 map
。app
它接受一個參數:一個方法,在每個數組元素上調用。而後返回一個新的數組,因此沒有反作用。dom
const numbers = [1, 2, 3, 4]
const numbersPlusOne = numbers.map(n => n + 1) // 每一個元素 +1
console.log(numbersPlusOne) // [2, 3, 4, 5]
複製代碼
你也能建立一個新數組,用於保留對象的一個特殊屬性:函數式編程
const allActivities = [
{ title: 'My activity', coordinates: [50.123, 3.291] },
{ title: 'Another activity', coordinates: [1.238, 4.292] },
// etc.
]
const allCoordinates = allActivities.map(activity => activity.coordinates)
console.log(allCoordinates) // [[50.123, 3.291], [1.238, 4.292]]
複製代碼
因此,請記住,當你須要去轉換數組時,考慮使用map。函數
這個方法的名字在這裏十分準確的:當你想去過濾數組的時候使用它。工具
如同map
所作,它接受一個函數做爲它的惟一參數,在數組的每一個元素上調用。這個方法返回一個布爾值:
true
若是你須要在數組中保留元素false
若是你不想保留它接着你會獲得一個帶有你想要保留的元素的新數組。
舉個例子,你能夠在數組中只保留奇數:
const numbers = [1, 2, 3, 4, 5, 6]
const oddNumbers = numbers.filter(n => n % 2 !== 0)
console.log(oddNumbers) // [1, 3, 5]
複製代碼
或者你能夠在數組中移除特殊的項:
const participants = [
{ id: 'a3f47', username: 'john' },
{ id: 'fek28', username: 'mary' },
{ id: 'n3j44', username: 'sam' },
]
function removeParticipant(participants, id) {
return participants.filter(participant => participant.id !== id)
}
console.log(removeParticipant(participants, 'a3f47')) // [{ id: 'fek28', username: 'mary' }, { id: 'n3j44', username: 'sam' }];
複製代碼
我的認爲是最難理解的方法。可是若是你一旦掌握它,不少瘋狂的事情你均可以用它作到。
基本地, reduce
使用有值的數組而後組合成一個新的值。它接受兩個參數,一個回調方法就是咱們的 reducer 和一個可選的初始化的值(默認是數組的第一個項)。這個 reducer 本身使用四個參數:
reduce
的數組大多數時候,你只須要使用前兩個參數:累計值和當前值。
拋開這些理論。來看看常見的一個 reduce
的例子。
const numbers = [37, 12, 28, 4, 9]
const total = numbers.reduce((total, n) => total + n)
console.log(total) // 90
複製代碼
在第一個遍歷時,這個累計值,也就是 total
,使用了初始化爲 37 的值。它返回的值是 37 + n
而且 n
等於 12,所以獲得 49.在第二次遍歷時,累加值是 49,返回值是 49 + 28 = 77。如此繼續直到第四次。
reduce
是很強大的,你能夠實際使用它去構建不少數組的方法,好比 map
或者 filter
:
const map = (arr, fn) => {
return arr.reduce((mappedArr, element) => {
return [...mappedArr, fn(element)]
}, [])
}
console.log(map([1, 2, 3, 4], n => n + 1)) // [2, 3, 4, 5]
const filter = (arr, fn) => {
return arr.reduce((filteredArr, element) => {
return fn(element) ? [...filteredArr] : [...filteredArr, element]
}, [])
}
console.log(filter([1, 2, 3, 4, 5, 6], n => n % 2 === 0)) // [1, 3, 5]
複製代碼
根本上看,咱們給 reduce
一個初始默認值 []
:咱們的累計值。對於 map
,咱們運行一個方法,它的結果是累加到最後,多虧了 展開操做符(沒必要擔憂,後面討論)。對於 filter
,幾乎是類似的,除了咱們在元素上運行過濾函數。若是返回 true,咱們返回前一個數組,不然在數組最後添加當前元素。
咱們來看一個更高級的例子:深度展開數組,也就是說把 [1, 2, 3, [4, [[[5, [6, 7]]]], 8]]
樣的數組轉換成 [1, 2, 3, 4, 5, 6, 7, 8]
樣的。
function flatDeep(arr) {
return arr.reduce((flattenArray, element) => {
return Array.isArray(element)
? [...flattenArray, ...flatDeep(element)]
: [...flattenArray, element]
}, [])
}
console.log(flatDeep([1, 2, 3, [4, [[[5, [6, 7]]]], 8]])) // [1, 2, 3, 4, 5, 6, 7, 8]
複製代碼
這個例子有點像 map
,除了咱們用到了遞歸。我不想去解釋這個用法,它超出了這篇文章的範圍。可是,若是你想了解更多的關於遞歸的知識,請參考這篇優質的文章。
我知道這不是一個方法。可是,在處理數組時,使用展開操做能夠幫助你作不少事情。事實上,你能夠在另外一個數組中使用它展開一個數組的值。從這一點來講,你能夠複製一個數組,或者鏈接多個數組。
const numbers = [1, 2, 3]
const numbersCopy = [...numbers]
console.log(numbersCopy) // [1, 2, 3]
const otherNumbers = [4, 5, 6]
const numbersConcatenated = [...numbers, ...otherNumbers]
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]
複製代碼
注意::展開操做符對原數組作了一次淺拷貝。但什麼是 淺拷貝?🤔
額,淺拷貝是儘量少的複製原數組。當你有一個數組包含數字,字符串或者布爾值(基本類型),它們是沒問題的,這些值被真正複製。然而,對於 對象和數組 而言,這是不一樣的。只有 對原值的引用 會被複制!所以,若是你建立一個包含對象的數組的淺拷貝,而後在拷貝的數組中修改了對象,它也會修改原數組的對象,由於它們是 同一個引用。
const arr = ['foo', 42, { name: 'Thomas' }]
let copy = [...arr]
copy[0] = 'bar'
console.log(arr) // No mutations: ["foo", 42, { name: "Thomas" }]
console.log(copy) // ["bar", 42, { name: "Thomas" }]
copy[2].name = 'Hello'
console.log(arr) // /!\ MUTATION ["foo", 42, { name: "Hello" }]
console.log(copy) // ["bar", 42, { name: "Hello" }]
複製代碼
因此,若是你想去「真正地」拷貝一個包含對象或者數組的數組,你可使用 lodash 的方法 cloneDeep。可是不要以爲必須作這樣的事。這裏的目標是 意識到事情是如何運做的。
下面你看到的方法,是最好了解一下的,同時它們能幫助你解決某些問題,好比在數組中搜索一個元素,取出數組的部分或者更多。
你曾經嘗試用過 indexOf
去查找一個數組中是否存在某個東西嗎?這是一個糟糕的方式對吧?幸運的是,includes
爲咱們作到了這些。給 includes
一個參數,而後會在數組裏面搜索它,若是一個元素存在的話。
const sports = ['football', 'archery', 'judo']
const hasFootball = sports.includes('football')
console.log(hasFootball) // true
複製代碼
concat 方法能夠用來合併兩個或者更多的數組。
const numbers = [1, 2, 3]
const otherNumbers = [4, 5, 6]
const numbersConcatenated = numbers.concat(otherNumbers)
console.log(numbersConcatenated) // [1, 2, 3, 4, 5, 6]
// You can merge as many arrays as you want
function concatAll(arr, ...arrays) {
return arr.concat(...arrays)
}
console.log(concatAll([1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12])) // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
複製代碼
不管什麼時候你想爲數組的每一個元素執行一些事情時,可使用 forEach
。它使用一個函數做爲參數,而後給它三個參數:當前值,索引,和當前數組。
const numbers = [1, 2, 3, 4, 5]
numbers.forEach(console.log)
// 1 0 [ 1, 2, 3 ]
// 2 1 [ 1, 2, 3 ]
// 3 2 [ 1, 2, 3 ]
複製代碼
這個用來在給定的數組中找出第一個被發現的元素的索引。 indexOf
也普遍用於檢查元素是否在一個數組中。不過老實說,我現在已經不這樣使用了。
const sports = ['football', 'archery', 'judo']
const judoIndex = sports.indexOf('judo')
console.log(judoIndex) // 2
複製代碼
find
方法十分相似於 filter
方法。你必須提供一個函數用於測試數組的元素。然而,find
一旦發現有一個元素經過測試,就當即中止測試其餘元素。不用於 filter
,filter
將會迭代整個數組,不管狀況如何。
const users = [
{ id: 'af35', name: 'john' },
{ id: '6gbe', name: 'mary' },
{ id: '932j', name: 'gary' },
]
const user = users.find(user => user.id === '6gbe')
console.log(user) // { id: '6gbe', name: 'mary' }
複製代碼
因此使用 filter
,當你想去過濾整個數組時。使用 find
在當你肯定在數組中找某個惟一元素的時候。
這個方法徹底跟 find
相同除了它返回第一個發現元素的索引,而不是直接返回元素。
const users = [
{ id: 'af35', name: 'john' },
{ id: '6gbe', name: 'mary' },
{ id: '932j', name: 'gary' },
]
const user = users.findIndex(user => user.id === '6gbe')
console.log(user) // 1
複製代碼
你或許認爲 findIndex
跟 indexOf
是相同的。額……不徹底是。indexOf
的第一個元素是基本值(布爾,數字,字符串,null,undefined或者一個 symbol)而findIndex
的第一個元素是一個回調方法。
因此當你須要搜索在數組中的一個元素的基本值時,使用 indexOf
。若是有更復雜的元素,好比object,使用 findIndex
。
當你須要取出或者複製數組的一部分,可使用 slice
。可是注意,像展開操做符同樣, slice
返回部分的淺拷貝!
const numbers = [1, 2, 3, 4, 5]
const copy = numbers.slice()
複製代碼
我在文章的開始談到,循環是沒有什麼用的。來用一個例子說明你如何擺脫它。
假設你想去從 API 中去除必定量的聊天記錄裏,而後展現它們中的 5 條。有兩種方式實現:一種是循環,另外一種是 slice
。
// 傳統方式
// 用循環來決定消息的數量
const nbMessages = messages.length < 5 ? messages.length : 5
let messagesToShow = []
for (let i = 0; i < nbMessages; i++) {
messagesToShow.push(posts[i])
}
// 假設 arr 少於 5 個元素
// slice 將會返回原數組的整個淺拷貝
const messagesToShow = messages.slice(0, 5)
複製代碼
若是你想測試數組中 至少有一個元素 經過測試,那麼可使用 some
。就像是 map
,filter
,和 find
,some
用回調函數做爲參數。它返回 ture
,若是至少一個元素經過測試,返回 true
不然返回 false
。
當你處理權限問題的時候,可使用 some
:
const users = [
{
id: 'fe34',
permissions: ['read', 'write'],
},
{
id: 'a198',
permissions: [],
},
{
id: '18aa',
permissions: ['delete', 'read', 'write'],
},
]
const hasDeletePermission = users.some(user =>
user.permissions.includes('delete')
)
console.log(hasDeletePermission) // true
複製代碼
相似 some
,不一樣的是 ever
測試了全部的元素是否知足條件(而不是 至少一個)。
const users = [
{
id: 'fe34',
permissions: ['read', 'write'],
},
{
id: 'a198',
permissions: [],
},
{
id: '18aa',
permissions: ['delete', 'read', 'write'],
},
]
const hasAllReadPermission = users.every(user =>
user.permissions.includes('read')
)
console.log(hasAllReadPermission) // false
複製代碼
這是一個即將到來的招牌方法, 在JavaScript 世界中。大體而言,flat
穿件一個新數組,經過組合全部的子數組元素。接受一個參數,數值類型,表明你想展開的深度。
const numbers = [1, 2, [3, 4, [5, [6, 7]], [[[[8]]]]]]
const numbersflattenOnce = numbers.flat()
console.log(numbersflattenOnce) // [1, 2, 3, 4, Array[2], Array[1]]
const numbersflattenTwice = numbers.flat(2)
console.log(numbersflattenTwice) // [1, 2, 3, 4, 5, Array[2], Array[1]]
const numbersFlattenInfinity = numbers.flat(Infinity)
console.log(numbersFlattenInfinity) // [1, 2, 3, 4, 5, 6, 7, 8]
複製代碼
猜猜這個方法幹什麼?我打賭你能夠作到顧名思義。
首先在每一個元素上運行一個 mapping 方法。接着一次性展現數據。十分簡單!
const sentences = [
'This is a sentence',
'This is another sentence',
"I can't find any original phrases",
]
const allWords = sentences.flatMap(sentence => sentence.split(' '))
console.log(allWords) // ["This", "is", "a", "sentence", "This", "is", "another", "sentence", "I", "can't", "find", "any", "original", "phrases"]
複製代碼
這個例子中,數組裏有一些句子,然而咱們想獲得全部的單詞。不使用 map
去把全部的句子分割成單詞而後展開數組,你能夠直接使用 flatMap
。
與 flatMap
無關的,你可使用 reduce
方法來計算單詞的數量(只是展現另外一種 reduce
的用法)
const wordsCount = allWords.reduce((count, word) => {
count[word] = count[word] ? count[word] + 1 : 1
return count
}, {})
console.log(wordsCount) // { This: 2, is: 2, a: 1, sentence: 2, another: 1, I: 1, "can't": 1, find: 1, any: 1, original: 1, phrases: 1, }
複製代碼
flatMap
常常用於響應式編程,這裏有個例子。
若是你須要基於數組元素建立字符串,join
正是你所尋找的。它容許經過連接數組元素來建立一個新的字符串,經過提供的分割符分割。
舉個例子,你可使用 join
一眼展現活動的參與者:
const participants = ['john', 'mary', 'gary']
const participantsFormatted = participants.join(', ')
console.log(participantsFormatted) // john, mary, gary
複製代碼
下面的例子更真實,在於你想先過濾參與者而後獲得他們的名字。
const potentialParticipants = [
{ id: 'k38i', name: 'john', age: 17 },
{ id: 'baf3', name: 'mary', age: 13 },
{ id: 'a111', name: 'gary', age: 24 },
{ id: 'fx34', name: 'emma', age: 34 },
]
const participantsFormatted = potentialParticipants
.filter(user => user.age > 18)
.map(user => user.name)
.join(', ')
console.log(participantsFormatted) // gary, emma
複製代碼
這是一個靜態方法,從類數組中建立新的數組,或者像例子中的字符串同樣遍歷對象。當處理 dom 時,這個方法十分有用。
const nodes = document.querySelectorAll('.todo-item') // 這是一個 nodeList 實例
const todoItems = Array.from(nodes) // 如今你能使用 map filter 等等,就像在數組中那樣!
複製代碼
你曾經見到過咱們使用 Array
代替數組實例嗎?這就是問什麼 from
被稱做靜態方法。
接着能夠愉快處理這些節點,好比用 forEach
在每一個節點上註冊事件監聽:
todoItems.forEach(item => {
item.addEventListener('click', function() {
alert(`You clicked on ${item.innerHTML}`)
})
})
複製代碼
下面是其餘常見的數組方法。不一樣之處在於,它們會修改原數組。修改數組並無什麼錯,最好是你應該有意識去修改它。
對於這些方法,若是你不想去改變原數組,只能在操做前淺拷貝或者深拷貝。
const arr = [1, 2, 3, 4, 5]
const copy = [...arr] // or arr.slice()
複製代碼
是的,sort
修改了原數組。事實上,在這裏進行了數組元素排序。默認的排序方法把全部的元素轉換成字符串,而後按照字母表排序它們。
const names = ['john', 'mary', 'gary', 'anna']
names.sort()
console.log(names) // ['anna', 'gary', 'john', 'mary']
複製代碼
若是你有 Python 背景的話,要當心了。使用 sort
在數字數組中不會獲得你想要的結果。
const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort()
console.log(numbers) // [12, 17, 187, 23, 3, 90] 🤔
複製代碼
那麼如何對一個數組排序?額,sort
接受一個函數,一個比較函數。這個函數接受兩個參數:第一個元素(咱們稱呼爲 a
)和第二個元素做比較(b
)。這兩個元素之間的比較須要返回一個數字。
a
排序在 b
以前。b
排序在 a
以前。那麼你可使用下面的方式排序數組:
const numbers = [23, 12, 17, 187, 3, 90]
numbers.sort((a, b) => a - b)
console.log(numbers) // [3, 12, 17, 23, 90, 187]
複製代碼
或者經過最近時間排序:
const posts = [
{
title: 'Create a Discord bot under 15 minutes',
date: new Date(2018, 11, 26),
},
{ title: 'How to get better at writing CSS', date: new Date(2018, 06, 17) },
{ title: 'JavaScript arrays', date: new Date() },
]
posts.sort((a, b) => a.date - b.date) // Substracting two dates returns the difference in millisecond between them
console.log(posts)
// [ { title: 'How to get better at writing CSS',
// date: 2018-07-17T00:00:00.000Z },
// { title: 'Create a Discord bot under 15 minutes',
// date: 2018-12-26T00:00:00.000Z },
// { title: 'Learn Javascript arrays the functional way',
// date: 2019-03-16T10:31:00.208Z } ]
複製代碼
fill
修改或者填充了數組的全部元素,從開始索引到結束索引,使用一個靜態值。fill
最有用的做用是使用靜態值填充一個新數組。
// Normally I would have called a function that generates ids and random names but let's not bother with that here.
function fakeUser() {
return {
id: 'fe38',
name: 'thomas',
}
}
const posts = Array(3).fill(fakeUser())
console.log(posts) // [{ id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }, { id: "fe38", name: "thomas" }]
複製代碼
這個方法名在這裏顯而易見。然而,像留意 sort
那樣,reverse
會反轉數組的位置。
const numbers = [1, 2, 3, 4, 5]
numbers.reverse()
console.log(numbers) // [5, 4, 3, 2, 1]
複製代碼
終於,在這個最後的部分,你將發現改變原數組的方法,同時能夠很容易替換其中一些。我不是說你應該拋棄這些方法。只是想要你意識到一些數組方法有反作用,而且這裏有可選擇的其餘方法。
處理數組時這是使用最多的方法。事實上,push
容許你在數組中添加一個或者多個元素。它也一般基於一箇舊數組構建一個新數組。
const todoItems = [1, 2, 3, 4, 5]
const itemsIncremented = []
for (let i = 0; i < items.length; i++) {
itemsIncremented.push(items[i] + 1)
}
console.log(itemsIncremented) // [2, 3, 4, 5, 6]
const todos = ['Write an article', 'Proofreading']
todos.push('Publish the article')
console.log(todos) // ['Write an article', 'Proofreading', 'Publish the article']
複製代碼
若是你須要像 itemsIncremented
同樣構建一個數組,不少方法都是機會,像咱們的朋友 map
,filter
或者reduce
。事實上咱們可使用 map
一樣作到:
const itemsIncremented = todoItems.map(x => x + 1)
複製代碼
而且若是你須要使用 push
,當你要添加新元素的時候,展開操做符爲你撐腰。
const todos = ['Write an article', 'Proofreading']
console.log([...todos, 'Publish the article']) // ['Write an article', 'Proofreading', 'Publish the article']
複製代碼
splice
經常用於做爲移除某個索引元素的方法。你能夠一樣使用 filter
作到。
const months = ['January', 'February', 'March', 'April', ' May']
// With splice
months.splice(2, 1) // remove one element at index 2
console.log(months) // ['January', 'February', 'April', 'May']
// Without splice
const monthsFiltered = months.filter((month, i) => i !== 3)
console.log(monthsFiltered) // ['January', 'February', 'April', 'May']
複製代碼
你可能會想,若是我須要移除多個元素呢?額,使用 slice
:
const months = ['January', 'February', 'March', 'April', ' May']
// With splice
months.splice(1, 3) // remove thirds element starting at index 1
console.log(months) // ['January', 'May']
// Without splice
const monthsFiltered = [...months.slice(0, 1), ...months.slice(4)]
console.log(monthsFiltered) // ['January', 'May']
複製代碼
shift
移除數組的第一個元素而後返回它。從功能上來講,你可使用 spread/rest 實現。
const numbers = [1, 2, 3, 4, 5]
// With shift
const firstNumber = numbers.shift()
console.log(firstNumber) // 1
console.log(numbers) // [2, 3, 4, 5]
// Without shift
const [firstNumber, ...numbersWithoutOne] = numbers
console.log(firstNumber) // 1
console.log(numbersWithoutOne) // [2, 3, 4, 5]
複製代碼
Unshift 容許你在數組開始添加一個或者多個元素。像是 shift
, 你可使用展開操做符作一樣的事:
const numbers = [3, 4, 5]
// With unshift
numbers.unshift(1, 2)
console.log(numbers) // [1, 2, 3, 4, 5]
// Without unshift
const newNumbers = [1, 2, ...numbers]
console.log(newNumbers) // [1, 2, 3, 4, 5]
複製代碼
for-loop
也不要重複造輪子,你想作的可能已經有一個方法在那裏。map
,filter
,reduce
和展開操做符。它們對開發者來講是最基礎的工具。slice
,some
,flatMap
等等。記住它們而且在合適的時候使用它們。slice
和展開操做符是淺拷貝。所以,對象和子數組將會共享同一個引用,當心使用它們。