让咱们来作一个大胆的声明: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]
(完)