【译】你应该了解的JavaScript数组方法

让我们来做一个大胆的声明:for循环通常是无用的,而且还导致代码难以理解。当涉及迭代数组、查找元素、或对其排序或者你想到的任何东西,都可能有一个你可以使用的数组方法。

然而,尽管这些方法很有用,但是其中一些仍然没有被人所熟知和使用。我将会为你介绍一些有用的方法。可以将这篇文章作为你学习JavaScript数组方法的指南。

注:在开始之前,你需要知道一件事情:我对于函数式编程有偏见。所以我倾向使用不直接改变原始数组的方法。这样,我避免了副作用。我不是说你永远不应该改变一个数组,但至少要知道有些方法可以做到这点并且会导致副作用。副作用会导致一些不必要的改变,而不必要的改变会导致BUG.

好了,知道这点,我们就开始吧。

要点

当你使用数组时,你将会想知道这四个方法,分别是: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方法那样,filter方法会接收一个函数,数组中的每个元素都会去调用这个函数。这个函数需要返回一个布尔值:

  • 返回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中返回的值。
  • 数组当前值
  • 当前索引
  • The array reduce was called upon(暂未理解)
    大多数情况下,你只需用到前面两个参数:累加器和当前值。
    我们不用过于理论化了,下面是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,只是我们这里使用了递归。我不会去解析这个代码,因为超出了这篇文章的范围。如果你想了解更多关于递归的知识,可以查看这里的优秀资源

展开运算符(ES6)

我同意,这其实不是一个方法。但是使用展开运算符可以帮组你在使用数组时实现很多功能。实际上,你可以用来展开一个数组中的数组值。因此,你也可以用来复制数组或合并多个数组。

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可以实现同样的事情。incluses方法接收一个参数,将会搜索数组中是否存在这个元素。

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方法一旦找到测试通过的元素,就会停止测试。而不是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方法。

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

你可能认为这findIndex是一样的indexOf。嗯...不完全是。第一个参数indexOf是原始值(boolean, number, string, null, undefined or a symbol),而第一个参数findIndex是回调函数。

因此,当你需要在原始值数组中搜索元素的索引时,可以使用indexOf。如果您有更复杂的元素,如对象,请使用findIndex。

slice

无论何时需要获取数组的一部分或复制数组,都可以使用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

如果你想测试一个数组的至少一个元素通过测试,你可以使用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

every

类似于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

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)

你能猜出这种方法有用吗?我打赌你可以用这个名字。

首先,它在每个元素上运行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。它允许通过连接所有数组的元素来创建一个新的字符串,由提供的分隔符分隔。
例如,你可以使用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

from

这是一个静态方法,可以从类似数组或可迭代的对象(例如字符串)创建新的数组。当你使用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

在这里,让我们来谈谈另一种数组的静态方法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

是的,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]

pop

从数组中删除最后一个元素并返回它。

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

使用数组时,这是一种广泛使用的方法。实际上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通常用作删除某个索引处元素的方法。你实际上也使用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

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]

(完)

原文

https://thomlom.dev/what-you-should-know-about-js-arrays/

posted @ 2019-04-14 21:48  Aarongo  阅读(591)  评论(0编辑  收藏  举报