【譯】關於JavaScript 陣列你應該知道的事情

toddmark發表於2019-04-04

首先做一個粗體宣告:迴圈經常是無用的,並且使得程式碼很難閱讀。 當談到迭代一個陣列的時候,無論你想去查詢元素,排序或者任何其他的事,都有可能存在一個陣列的方法供你使用。

然而,儘管它們有用,但其中一些仍然不被人瞭解。我會努力為你展示一些有用的方法。把這篇文章當做對 JavaScript 陣列方法的指引吧。

注意: 在開始之前,不得不瞭解一件事:我比較偏愛函數語言程式設計。所以我傾向於使用的方法不會直接改變原來的陣列。這種方法,我避免了副作用。我不是說不應該改變陣列,但至少要了解那些方法會改變,那些會有副作用。副作用導致不想要的改變,而不想要的改變帶來bugs!

瞭解到這裡,我們可以開始正文了。

必不可少的

當跟陣列打交道時,有四件事你應該清楚:map,filter,reduce 和 展開操作符。它們富有力量。

map

你可以在很多種情況下使用它。基本地,每次你需要修改陣列的元素時,考慮使用 map

它接受一個引數:一個方法,在每一個陣列元素上呼叫。然後返回一個新的陣列,所以沒有副作用。

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

filter

這個方法的名字在這裡十分準確的:當你想去過濾陣列的時候使用它。

如同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

個人認為是最難理解的方法。但是如果你一旦掌握它,很多瘋狂的事情你都可以用它做到。

基本地, reduce 使用有值的陣列然後組合成一個新的值。它接受兩個引數,一個回撥方法就是我們的 reducer 和一個可選的初始化的值(預設是陣列的第一個項)。這個 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,除了我們用到了遞迴。我不想去解釋這個用法,它超出了這篇文章的範圍。但是,如果你想了解更多的關於遞迴的知識,請參考這篇優質的文章

展開操作(ES2015)

我知道這不是一個方法。但是,在處理陣列時,使用展開操作可以幫助你做很多事情。事實上,你可以在另一個陣列中使用它展開一個陣列的值。從這一點來說,你可以複製一個陣列,或者連線多個陣列。

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。但是不要覺得必須做這樣的事。這裡的目標是 意識到事情是如何運作的

最好了解的

下面你看到的方法,是最好了解一下的,同時它們能幫助你解決某些問題,比如在陣列中搜尋一個元素,取出陣列的部分或者更多。

includes(ES2015)

你曾經嘗試用過 indexOf 去查詢一個陣列中是否存在某個東西嗎?這是一個糟糕的方式對吧?幸運的是,includes 為我們做到了這些。給 includes 一個引數,然後會在陣列裡面搜尋它,如果一個元素存在的話。

const sports = ['football', 'archery', 'judo']
const hasFootball = sports.includes('football')
console.log(hasFootball) // true
複製程式碼

concat

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

無論何時你想為陣列的每個元素執行一些事情時,可以使用 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

這個用來在給定的陣列中找出第一個被發現的元素的索引。 indexOf 也廣泛用於檢查元素是否在一個陣列中。不過老實說,我如今已經不這樣使用了。

const sports = ['football', 'archery', 'judo']

const judoIndex = sports.indexOf('judo')
console.log(judoIndex) // 2
複製程式碼

find

find 方法十分類似於 filter 方法。你必須提供一個函式用於測試陣列的元素。然而,find 一旦發現有一個元素通過測試,就立即停止測試其他元素。不用於 filterfilter 將會迭代整個陣列,無論情況如何。

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 在當你確定在陣列中找某個唯一元素的時候。

findIndex

這個方法完全跟 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
複製程式碼

你或許認為 findIndexindexOf 是相同的。額……不完全是。indexOf 的第一個元素是基本值(布林,數字,字串,null,undefined或者一個 symbol)而findIndex的第一個元素是一個回撥方法。

所以當你需要搜尋在陣列中的一個元素的基本值時,使用 indexOf。如果有更復雜的元素,比如object,使用 findIndex

slice

當你需要取出或者複製陣列的一部分,可以使用 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

如果你想測試陣列中 至少有一個元素 通過測試,那麼可以使用 some。就像是 map,filter,和 findsome 用回撥函式作為引數。它返回 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
複製程式碼

every

類似 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
複製程式碼

flat(ES2019)

這是一個即將到來的招牌方法, 在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]
複製程式碼

flatMap(ES2019)

猜猜這個方法幹什麼?我打賭你可以做到顧名思義。

首先在每個元素上執行一個 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 正是你所尋找的。它允許通過連結陣列元素來建立一個新的字串,通過提供的分割符分割。

舉個例子,你可以使用 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
複製程式碼

from

這是一個靜態方法,從類陣列中建立新的陣列,或者像例子中的字串一樣遍歷物件。當處理 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

是的,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 之前。
  • 如果是0,沒有任何改變。

那麼你可以使用下面的方式排序陣列:

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 修改或者填充了陣列的所有元素,從開始索引到結束索引,使用一個靜態值。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" }]
複製程式碼

reverse

這個方法名在這裡顯而易見。然而,像留意 sort 那樣,reverse 會反轉陣列的位置。

const numbers = [1, 2, 3, 4, 5]

numbers.reverse()
console.log(numbers) // [5, 4, 3, 2, 1]
複製程式碼

你可以替換的方法

終於,在這個最後的部分,你將發現改變原陣列的方法,同時可以很容易替換其中一些。我不是說你應該拋棄這些方法。只是想要你意識到一些陣列方法有副作用,並且這裡有可選擇的其他方法。

push

處理陣列時這是使用最多的方法。事實上,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

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

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

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 和展開操作符是淺拷貝。因此,物件和子陣列將會共享同一個引用,小心使用它們。
  • “舊”的改變陣列的方法可以被新的替換。取決於你想做什麼。

pic

相關文章