讓咱們來作一個大膽的聲明:for循環一般是無用的,並且還致使代碼難以理解。當涉及迭代數組、查找元素、或對其排序或者你想到的任何東西,均可能有一個你可使用的數組方法。html
然而,儘管這些方法頗有用,可是其中一些仍然沒有被人所熟知和使用。我將會爲你介紹一些有用的方法。能夠將這篇文章做爲你學習JavaScript數組方法的指南。node
注:在開始以前,你須要知道一件事情:我對於函數式編程有偏見。因此我傾向使用不直接改變原始數組的方法。這樣,我避免了反作用。我不是說你永遠不該該改變一個數組,但至少要知道有些方法能夠作到這點而且會致使反作用。反作用會致使一些沒必要要的改變,而沒必要要的改變會致使BUG.react
好了,知道這點,咱們就開始吧。es6
當你使用數組時,你將會想知道這四個方法,分別是:map、filter、reduce和展開運算符。它們很是強大及實用。編程
你會常用到這個方法。基本上,每次你須要修改數組元素的時候,應該考慮使用map。
該方法接收一個參數:一個在數組每一個元素上調用的方法。而且會返回一個新的數組,因此沒有產生反作用。數組
const numbers = [1, 2, 3, 4] const numbersPlusOne = numbers.map(n => n + 1) // 每一個元素值都加1 console.log(numbersPlusOne) // [2, 3, 4, 5]
你也能夠建立一個新的數組,只保留對象其中的一個特定屬性值。app
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。less
這裏方法的名字表示的很明確了:當你想篩選數組時候就可使用它。
如同map方法那樣,filter方法會接收一個函數,數組中的每一個元素都會去調用這個函數。這個函數須要返回一個布爾值:dom
而後你就會獲得你想要的一個新數組了。
好比下面例子,你能夠只保留數組中的奇數函數式編程
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自己有四個參數:
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能夠實現一樣的事情。incluses方法接收一個參數,將會搜索數組中是否存在這個元素。
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是原始值(boolean, number, string, null, undefined or a symbol),而第一個參數findIndex是回調函數。
所以,當你須要在原始值數組中搜索元素的索引時,可使用indexOf。若是您有更復雜的元素,如對象,請使用findIndex。
不管什麼時候須要獲取數組的一部分或複製數組,均可以使用slice。但要當心,就像展開運算符同樣,slice返回該部分的淺拷貝!
在文章的開頭我說過,for循環一般是無用的。讓我舉個例子說明如何擺脫它。
假設您想要從API檢索必定數量的聊天消息,而且你只想顯示其中的五個。有兩種方法能夠實現:一個帶有for循環另外一個帶有slice。
// The "traditional way" to do it: // Determine the number of messages to take and use a for loop const nbMessages = messages.length < 5 ? messages.length : 5 let messagesToShow = [] for (let i = 0; i < nbMessages; i++) { messagesToShow.push(posts[i]) } // Even if "arr" has less than 5 elements, // slice will return an entire shallow copy of the original array const messagesToShow = messages.slice(0, 5)
若是你想測試一個數組的至少一個元素經過測試,你可使用some。就像map,filter或者find,some將回調函數做爲其惟一參數。若是至少一個元件經過測試則返回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方法,不一樣之處在於every方法測試全部元素是否經過測試(而不是至少一個)
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]
你能猜出這種方法有用嗎?我打賭你能夠用這個名字。
首先,它在每一個元素上運行map函數。而後它將數組一次展平。十分簡單!
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"]
在這個例子中,數組中有不少句子而且你想獲得全部的單詞。你能夠直接使用flatMap方法而不是使用map方法來分片這些句子爲單詞而後進行展平化。
一個跟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實現一目瞭然地顯示活動的全部參與者。
onst 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') // this is an instance of NodeList const todoItems = Array.from(nodes) // now, you can use map, filter, etc. as you're workin with an array!
是否看到咱們使用的是Array代替數組實例?這就是from被稱爲靜態方法的緣由。
而後你能夠操做這些節點,例如經過forEach在每一個節點上註冊一個事件監聽器:
todoItems.forEach(item => { item.addEventListener('click', function() { alert(`You clicked on ${item.innerHTML}`) }) })
在這裏,讓咱們來談談另外一種數組的靜態方法isArray。毫無心外,它會告訴你傳遞的值是否爲數組。
基於以上的例子,這裏咱們獲得:
const nodes = document.querySelectorAll('.todo-item') console.log(Array.isArray(nodes)) // false const todoItems = Array.from(nodes) console.log(Array.isArray(todoItems)) // true
在下面找到其餘常見的數組方法。不一樣之處在於它們修改了原始數組。改變數組沒有錯,但瞭解這個方法使用也是一件好事!
對於全部這些方法,若是你不想改變原始數組,只需事先製做淺拷貝或深拷貝:
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)。這兩個元素之間的比較須要返回一個數字:
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]
從數組中刪除最後一個元素並返回它。
const messages = ['Hello', 'Hey', 'How are you?', "I'm fine"] const lastMessage = messages.pop() console.log(messages) // ['Hello', 'Hey', 'How are you?'] console.log(lastMessage) // I'm fine
最後,在最後一節中,你將找到改變原始數組的方法,而且可使用其餘方法輕鬆替換。我不是說你應該放棄這些方法。我只是想讓你意識到一些數組方法有反作用,而且有替代方案👍
使用數組時,這是一種普遍使用的方法。實際上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一般用做刪除某個索引處元素的方法。你實際上也使用filte這樣作:
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 three elements starting at index 1 console.log(months) // ['January', 'May'] // Without splice const monthsSliced = [...months.slice(0, 1), ...months.slice(4)] console.log(monthsSliced) // ['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]
(完)