首先作一个粗体声明:循环常常是无用的,而且使得代码很难阅读。 当谈到迭代一个数组的时候,不管你想去查找元素,排序或者任何其余的事,都有可能存在一个数组的方法供你使用。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
和展开操做符是浅拷贝。所以,对象和子数组将会共享同一个引用,当心使用它们。