我的后现代数组常用方法

156 阅读7分钟

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 几乎一致,也同样不能够中断。

其次,mapforEach 最主要的区别就是 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> ]
    
  • 如果没有返回值就会返回 undefined

    const 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)

最强大的莫过于组合函数,也类似于柯里化,但是我现在写不出来哈哈哈哈哈哈哈哈哈,再说吧。

还有一个函数 reduceRightreduce 一样,但是执行方向是从后往前,换个意思就是先执行了一次 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 类似,即它接收一个函数,并且函数一个布尔值。

另外返回值方面 findIndexindexOf (假装大家都会用)类似,如果在数组中找到了该元素,就返回该元素下标,没有的话就返回 -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

其他

还有一些数组方法没有写,不是没有用到,就是用的比较少,以后有空我给补上。

另外有一点需要说明,基本上所有的操作函数都允许接收三个参数(少数例外,比如 reducereduceRight 都是接收四个的),第一个是当前元素,第二个是当前下标,第三个是数组本身,注意这个数组本身,它不是一个深浅考本的内容,而是指的堆中的同一个内存,所以当我们在遍历过程中修改了数组时,它会直接影响到后面的遍历结果。有兴趣的可以试一下(我没有试过全部的,这个只是猜测)。

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]

要是有什么错误,希望各位不要手下留情,指出我的错误吧。

参考

Array.prototype.map 源码分析 - 知乎(zhihu.com)

JavaScript 中的相等性判断 - JavaScript | MDN (mozilla.org)