ES6 都多少年了,还有多少同学不会 ES5 的数组函数?
下面我给大家介绍一下我常用的数组方法吧。
forEach()
说实话这个方法真的好久没有用到了,但是这篇文章后面老是用到这个实现其他方法,就先写了下来。
简单来说这就是一个数组遍历方法,或者说所有的数组方法都是遍历方法,只不过处理方式不一样罢了。
它接收一个函数,没有返回值,并且不允许中断。
为什么
来说一说出现 forEach 之前我们是怎么遍历一个数组的:
const arr = [1,2,3]
for(let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
是不是很麻烦,还要写一个 for 循环,不麻烦啊,那你喜欢咯。
怎么用
至于用法,dddd
const arr = [1,2,3]
arr.forEach((item) => {
console.log(item)
})
看吧,我们甚至不需要去写 arr[i] 去取得这个元素,直接使用 item 就可以拿到我们需要的元素。
当然,这个函数还可以接受第二个参数(当前下标)和第三个参数(遍历的数组)。至于怎么用吧,你去写写看咯。
本来不打算说返回值和中断的情况的,想了想还是要说一下,毕竟和 map 有区别。
let a = [1,2,3,4,5,6].forEach((item) => {
return item + 1;
console.log(item)// {1}
})
console.log(a)// {2}
你说这个在控制台打印出什么?
是的,你没有说错,只打印了一次 undefined,而且还是行{2}打印的,但函数确确实实执行了 6 次,只是遇到了 return 不再往下执行,而是进入了下一个循环。
所以 return 是没有办法中断 forEach 的。
那 break 呢?for 循环中 break 是可以跳出循环的吧。
不行,break 会直接抛出语法错误 Uncaught SyntaxError: Illegal break statement。
map()
说完 forEach,我们就来聊聊 map 这个数组遍历方法吧。
首先 map 的用法与 forEach 几乎一致,也同样不能够中断。
其次,map 与 forEach 最主要的区别就是 map 能够返回一个操作后的数组。
为什么
我们还是以遍历为例,假设我们需要对一个数组里面的每个元素都进行操作,并且获得每个操作之后的内容,在 forEach 的时候我们需要这么写:
const arr = [1,2,3]
const newArr = []
arr.forEach(item => {
newArr.push(item * 2)
})
console.log(newArr)
好像这么些也还好对吧,但是每个操作函数我们都使用了一个 push,这是一个额外操作,虽然大部分情况下这不会有什么问题,但是数据多起来还是有一点点影响性能的。
怎么用
这个时候就需要我们的 map 登场啦!
const newArr = arr.map(item => {
return item * 2
})
是不是代码更简洁了呢,我们还可以把代码简化成这样:
const newArr = arr.map(item => item * 2)
其实 map 的实际实现原理跟上面写的怎么用是一样的,但由于 map 是基于 V8 引擎实现的,所以在性能上会有优化。
另外需要说明一些问题
-
map 不会遍历空项
const arr = new Array(10) const newArr = arr.map(item => item + 1) // [ <10 empty items> ] -
如果没有返回值就会返回
undefinedconst arr = [0,1,2,3,4,5,6,7,8,9] const newArr = arr.map(item => { item + 1 }) // [ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined ]如果没有返回值的话,就没有必要使用
map了,mdn 也给出了提示:因为
map生成一个新数组,当你不打算使用返回的新数组却使用map是违背设计初衷的,请用forEach或者for-of替代。你不该使用map: A)你不打算使用返回的新数组,或/且 B) 你没有从回调函数中返回值。
filter()
顾名思义,就是过滤的意思。
依旧是接受一个操作函数,这个操作函数必须返回一个 boolean。当然不是也行,最后都会处理成 boolean 的🙃。所有的 falsely 值都会转换成 false,其他值则是 true。
它会返回一个执行结果都是 ture 的数组。
为什么
在没有 filter 之前我们是怎么实现过滤这个东西的呢?
还是用 forEach 来实现。为什么不用 for 循环?还不是我懒得写嘛!!!
const arr = [0,1,2,3,4,5,6,7,8,9]
const newArr = []
arr.forEach(item => {
if (item % 2) {
newArr.push(item)
}
})
是不是很不优雅?这是非常不优雅。、
map 实现不了这个东西,别问了,再问孩子傻了。
怎么用
用法那可就简单了,毕竟你都已经会 map 了不是吗?
const arr = [0,1,2,3,4,5,6,7,8,9]
const newArr = arr.filter(item => item % 2)
优雅,非常优雅。(对不起 亨利·韩德森校长)
includes()
包含,用来判断数组中是否存在某个元素。
接受一个任意值,是的你没有听错,只要数组中存在这个参数就会返回 true。
它还能接受第二个参数,指定从哪个位置开始。
为什么
我来直接实现一个吧,不晓得怎么讲了。
// 这里要注意不能用箭头函数,会导致下面的 this 拿不到值的。
Array.prototype.myIncludes = function (equal, start = 0) {
const len = this.length
if (len === 0) return false
if (start > len) return false
for(let i = start; i < len; i++) {
if (arr[i] === equal) return true
}
return false
}
const arr = [1,2,3,4,5]
arr.myIncludes(3) // true
arr.myIncludes(9) // false
其实我们实际要写的话也没有必要写在 prototype 上,我们也可以使得函数接收三个参数去实现:
function includes<T>(arr: T[], equal: T, start: number = 0) {
const len = arr.length
if (len === 0) return false
if (start > len) return false
for(let i = start; i < len; i++) {
if (arr[i] === equal) return true
}
return false
}
怎么用
嗯哼?上面有了,自己抄。
这里也有几个注意的点
-
includes()使用零值相等算法来确定是否找到给定的元素。[0].includes('0') // false [-0].includes(0) // true // 就连 NaN 相等也可以做到 [NaN].includes(NaN) / true -
没有办法比较引用类型的值
究其原因就是我们在栈上储存的引用地址不同。
当我们创建两个空数组时,我们认为时相等的,但是在程序中会为我们开辟两个内存来存放这两个数组。并将指向这两个内存的引用地址在栈中保存起来,所以实际上我们比较的时栈上面的值,而不是堆上的。
const arr1 = [] [arr1].includes(arr1) // true [arr1].includes([]) // false // 其他引用类型同理
reduce()
我斑愿称你为最强
讲真,大多数的数组方法都可以用 reduce 实现。
它接收一个操作函数,这个函数跟其他的遍历方法的操作函数不一样的是有四个参数,第一个参数是上一个操作函数的返回值。
它的第二个参数是默认值。
我们可以用它来实现上面的 map 操作:
const arr = [1,2,3]
const newArr = arr.reduce((acc, item) => {
acc.push(item)
return acc
}, [])
用它来实现 filter:
const arr = [1,2,3,4,5,6,7,8]
const newArr = arr.reduce((acc, item) => {
if (item % 2) acc.push(item)
return acc
}, [])
我们也可以用来做累加:
const arr = [1,2,3,4,5,6,7,8]
const newArr = arr.reduce((acc, item) => {
return acc + item
}, 0)
最强大的莫过于组合函数,也类似于柯里化,但是我现在写不出来哈哈哈哈哈哈哈哈哈,再说吧。
还有一个函数 reduceRight 跟 reduce 一样,但是执行方向是从后往前,换个意思就是先执行了一次 reverse
concat()
合并多个数组,接收多个参数,如果接收的是非数组,就会直接添加到数组的最后一个位置,如果是数组,就会逐个添加到数组中。
为什么
我们时常也会接到这种合并数组的需求,如果没有 concat,我们就需要自己去遍历。
const arr1 = [1,2,3,4]
const arr2 = [5,6,7,8]
function concat(...args) {
const result = []
for(let i = 0; i < args.length; i++) {
if (Array.isArray(args[i])) {
for(let j = 0; j < args[i].length; j++){
result.push(args[i][j])
}
} else {
result.push(args[i])
}
}
return result
}
这样的实现实在太不优雅了,而且时间复杂度为 O^2。
当然如果是 JQ 时代可以选择使用 jq 的 concat,但是我们 js 数组现在有自己的合并方法啦。
怎么用
我们只需要把所有想要合并的内容一起传入就可以了,
const arr1 = [1,2,3,4]
const arr2 = [5,6,7,8]
arr1.concat(arr2, 9)
它并不会修改到原数组,是把原数组克隆后作为基础数组去操作。
find()
find 接收一个判断函数,这个函数返回一个布尔值。
当然,for 循环一次也能够哈哈哈哈哈。
为什么
我们再用 forEach 来模拟一下 find 方法叭:
const arr = [
{ name: 'mike', age: '7' },
{ name: 'amy', age: '8' },
{ name: 'john', age: '7' }
]
let result = undefined;
arr.forEach((item) => {
if (item.name === 'mike') {
result = item
}
})
其实也还好,但还是那个问题,不够优雅。
const result = arr.find(item => item.name === 'mike') // {name: 'mike', age: '7'}
const result = arr.find(item => item.name === 'yell') // undefined
这样的一行可读性超高的代码难道不比上面的代码优雅吗?而且还是 js 提供的方法,为我们减少了好多性能问题。
find 只会返回一个数组元素,而不是一个数组,这个需要注意。
findIndex()
findIndex 一方面跟 find 类似,即它接收一个函数,并且函数一个布尔值。
另外返回值方面 findIndex 与 indexOf (假装大家都会用)类似,如果在数组中找到了该元素,就返回该元素下标,没有的话就返回 -1。
const arr = [
{ name: 'mike', age: '7' },
{ name: 'amy', age: '8' },
{ name: 'john', age: '7' }
]
arr.findIndex(item => item.name === 'mike') // 0
arr.findIndex(item => item.name === 'yell') // undefined
其他
还有一些数组方法没有写,不是没有用到,就是用的比较少,以后有空我给补上。
另外有一点需要说明,基本上所有的操作函数都允许接收三个参数(少数例外,比如 reduce、reduceRight 都是接收四个的),第一个是当前元素,第二个是当前下标,第三个是数组本身,注意这个数组本身,它不是一个深浅考本的内容,而是指的堆中的同一个内存,所以当我们在遍历过程中修改了数组时,它会直接影响到后面的遍历结果。有兴趣的可以试一下(我没有试过全部的,这个只是猜测)。
const arr = [1,2,3,4,5,6,7,8,9]
arr.map((item, index, arr) => {
arr[index + 1] += 2
return item * 2
})
console.log(arr)// [2, 8, 10, 12, 14, 16, 18, 20, 22]
要是有什么错误,希望各位不要手下留情,指出我的错误吧。