聊点JavaScript中的数组方法

时间:2022-09-13 20:27:08

前言

最近有同学在面试中被问到了这块,答的不是很好,所以决定临时写一期关于JavaScript数组方法这块的内容(没想到啊没想到,明明临近过年了,竟然还有个大需求,所以我才不会说是因为工作太忙,写代码比写文字省事呢)。

聊点JavaScript中的数组方法

1. 与数据结构相关的方法

有数据结构基础的同学应该都知道 栈 和 队列 这两种数据结构(不知道的同学可以自行查阅一下,暂无文章推荐~)。而 JavaScript 的数组方法中,有一些方法就是契合用数组实现的栈和队列这两种数据结构的方法的。

1.1 Array.prototype.push

push() 方法将一个或多个元素添加到数组的末尾,并返回该数组的新长度。

是否改变原数组:是

参数:

  • elementN:被添加到数组末尾的元素
  • 返回值:数组添加元素之后的 length 属性值

这个方法 类似 于 栈 中的 推入 方法,它将一个元素入栈:

  1. const stack = []; 
  2. stack.push(1);  // 1 
  3. console.log(stack); // [1] 
  4.  
  5. const stack1 = []; 
  6. stack1.push(1, 2); // 2 
  7. console.log(stack1); // [1, 2] 

1.2 Array.prototype.pop

pop()方法从数组中删除最后一个元素,并返回该元素的值。此方法更改数组的长度。

  • 是否改变原数组:是
  • 参数:无
  • 返回值:从数组中删除的元素(当数组为空时返回undefined)

这个方法 类似 于 栈 中的 弹出 方法,它将栈顶的元素弹出:

  1. const stack = [1, 2, 3, 4, 5]; 
  2. stack.pop(); // 5 
  3. console.log(stack); // [1, 2, 3, 4] 
  4.  
  5. const stack1 = []; 
  6. stack1.pop(); // undefined 
  7. console.log(stack1); // [] 

1.3 Array.prototype.unshift

unshift() 方法将一个或多个元素添加到数组的开头,并返回该数组的新长度(该方法修改原有数组)。

  • 是否改变原数组:是
  • 参数:
  • elementN:要添加到数组开头的元素或 多个 元素
  • 返回值:数组添加元素之后的 length 属性值

这个方法 有那么点 类似于 队列 的 入队 操作,它将一个或者多个元素入队:

  1. const queue = [3, 4, 5]; 
  2. queue.unshift(2); // 4 
  3. console.log(queue); // [2, 3, 4, 5] 
  4.  
  5. const queue1 = [3, 4, 5]; 
  6. queue1.unshift(1, 2); // 5 
  7. console.log(queue1); // [1, 2, 3, 4, 5] 

1.4 Array.prototype.shift

shift() 方法从数组中删除第一个元素,并返回该元素的值。此方法更改数组的长度。

  • 是否改变原数组:是
  • 参数:无
  • 返回值:从数组中删除的元素(当数组为空时返回undefined)

这个方法 有那么点 类似于 队列 的 出队 操作,也 有那么点 类似于 栈 的 弹出 操作:

  1. const queue = [3, 4, 5]; 
  2. queue.shift(); // 3 
  3. console.log(queue); // [4, 5] 
  4.  
  5. const queue1 = []; 
  6. queue1.shift(); // undefined 
  7. console.log(queue1); // [] 

1.5 小结

其实之所以说 类似于 或者 有点类似于 ,还是因为 JavaScript 中本身并没有提供严谨的实现队列或者栈这种数据结构的方法,但是通过这4个API的灵活使用,其实是可以比较灵活的实现类似于队列或者栈的功能的。但是如果真的要在生产环境中用这个4个API来模拟栈或者队列的话,不妨稍稍封装一下。感兴趣的同学不妨在学习栈以及队列的知识后来试试哦。

1.6 自己实现一下试试?

我们接着不妨来实现一下上面的API是怎么做的。

1.6.1 仿写 push

先来个 push 看看:

  1. Array.prototype.myPush = function(...args) { 
  2.   let top = this.length; 
  3.   for (let i = 0; i < args.length; i++) { 
  4.     this[top] = args[i]; 
  5.     top++; 
  6.   } 
  7.   return this.length; 
  8. }; 
  9.  
  10. const stack = []; 
  11. stack.myPush(1); // 1 
  12. console.log(stack); // [1] 
  13.  
  14. const stack1 = []; 
  15. stack1.myPush(1, 2); // 2 
  16. console.log(stack1); // [1, 2] 

注意:不要使用箭头函数,否则 this 的指向会出现一些问题,关于 this 指向的问题,因为不是本期的重点,所以暂时不聊。

1.6.2 仿写 pop

再试试 pop ?

  1. Array.prototype.myPop = function() { 
  2.   const topEle = this[this.length - 1]; 
  3.   this.length > 0 && this.length--; 
  4.   return topEle; 
  5. }; 
  6.  
  7. const stack = [1, 2, 3, 4, 5]; 
  8. stack.myPop(); // 5 
  9. console.log(stack); // [1, 2, 3, 4] 
  10.  
  11. const stack1 = []; 
  12. stack1.myPop(); // undefined 
  13. console.log(stack1); // [] 

1.7 小结

题外话,写一个方法,无论是仿写也好,还是自己发开也好。首先要确定的就是函数的入参和返回值,之后不再赘述。同时,我个人是倾向于都写纯函数的。至于 unshift 和 shift 的仿写,这里就不写了,会稍微麻烦一点,但是没有太复杂。

2. 和顺序有关的方法

2.1 Array.prototype.sort

sort() 方法用原地算法对数组的元素进行排序,并返回数组。默认排序顺序是在将元素转换为字符串,然后比较它们的UTF-16代码单元值序列时构建的。

是否改变原数组:是

参数:

  • compareFunction:用来指定按某种顺序进行排列的函数。如果省略,元素按照转换为的字符串的各个字符的Unicode位点进行排序。
  • 返回值:排序后的数组(原数组)

compareFunction

参数:

  • a:用于比较的第一个元素
  • b:用于比较的第二个元素

返回值:number

描述:

  • 如果 compareFunction(a, b) 小于 0 ,那么 a 会被排列到 b 之前
  • 如果 compareFunction(a, b) 等于 0 , a 和 b 的相对位置不变
  • 如果 compareFunction(a, b) 大于 0 , b 会被排列到 a 之前

简而言之, sort 方法接受一个回调函数。这个回调函数的入参为数组中待比较的两个元素a 和 b,返回值是一个 number , sort 根据 number 的值对数组进行如上描述的操作。

看个:chestnut::

  1. const numbers = [4, 2, 5, 1, 3]; 
  2. numbers.sort(function(a, b) { 
  3.   return a - b; 
  4. }); // [1, 2, 3, 4, 5] 

排序这个东西吧,要是展开说内容就太多了,这里就不展开了。

2.2 Array.prototype.reverse

reverse() 方法将数组中元素的位置颠倒,并返回该数组。数组的第一个元素会变成最后一个,数组的最后一个元素变成第一个。该方法会改变原数组。

  • 是否改变原数组:是
  • 参数:无
  • 返回值:颠倒后的数组(原数组)

没啥好说的,就是反转数组,那么来看个:chestnut::

  1. const arr = [1, 2, 3, 4, 5];  
  2. arr.reverse(); // [5, 4, 3, 2, 1] 

3. 和遍历相关的方法

3.1 Array.prototype.every

every() 方法测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。

是否改变原数组:否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值:boolean (是否 所有元素 都通过 callback 的测试)

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:boolean
  • 描述:判断当前元素是否满足 callback 的判断条件
  • 举个:chestnut::
  1. const test = [1, 2, 3, 4, 5];  
  2. const isMoreThanThree = (num) => { 
  3.   return num > 3; 
  4. };  
  5. test.every(isMoreThanThree); // false 

3.2 Array.prototype.some

some() 方法测试数组中是不是至少有1个元素通过了被提供的函数测试。它返回的是一个Boolean类型的值。

是否改变原数组:否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值:boolean (是否 至少有1个元素 通过了 callback 的测试)

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:boolean
  • 描述:判断当前元素是否满足 callback 的判断条件
  • 举个:chestnut::
  1. const test = [1, 2, 3, 4, 5]; 
  2.  
  3. const isMoreThanThree = (num) => { 
  4.   return num > 3; 
  5. }; 
  6.  
  7. test.some(isMoreThanThree); // true 

3.3 Array.prototype.find

find() 方法返回数组中满足提供的测试函数的 第一个元素的值 。否则返回 undefined。

是否改变原数组:否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值:数组中第一个满足所提供测试函数的元素的值,否则返回 undefined。

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:boolean
  • 描述:判断当前元素是否满足 callback 的判断条件
  • 举个:chestnut::
  1. const test = [1, 2, 3, 4, 5]; 
  2.  
  3. const isThree = (num) => { 
  4.   return num === 3; 
  5. }; 
  6.  
  7. const isSeven = (num) => { 
  8.   return num === 7; 
  9. }; 
  10.  
  11. test.find(isThree); // 3 
  12. test.find(isSeven); // undefined 

3.4 Array.prototype.findIndex

findIndex()方法返回数组中满足提供的测试函数的第一个元素的索引。若没有找到对应元素则返回-1。

和上面的非常类似,略。

3.5 Array.prototype.forEach

forEach() 方法对数组的每个元素执行一次给定的函数。

是否改变原数组: 否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值: undefined

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:不需要

想想自己在刚接触前端的时候,总是用错这个方法。无外乎是指令式的for循环用习惯了,并且编码习惯不好。其实新同学们可以这么记: 两个no——不改变原数组、没有返回值(返回值是undefined) 。好了,举个:chestnut::

  1. const test = [1, 2, 3, 4, 5];  
  2. const log = (val) => { 
  3.   console.log(val); 
  4. };  
  5. test.forEach(log);  // 依次打印 1 2 3 4 5 

3.6 Array.prototype.map

map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。

是否改变原数组: 否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值:Array(一个由原数组每个元素执行回调函数的结果组成的新数组。)

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:形成新数组的元素

曾经总是把这个方法与forEach傻傻分不清,其实现在来看,还是英文没学好。map 有映射的意思,简直是明示啊。所以说,map 它会生成一个与原数组有映射关系的数组嘛。那么举个:chestnut::

  1. const test = [1, 2, 3, 4, 5];  
  2. const double = (num) => { 
  3.   return num * 2; 
  4. }; 
  5.  
  6. const doubleList = test.map(double);  
  7. console.log(doubleList); // [2, 4, 6, 8, 10] 

3.7 Array.prototype.filter

filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。

是否改变原数组: 否

参数:

  • callback:回调函数
  • thisArg:执行 callback 时使用的 this 值
  • 返回值:Array(一个新的、由通过测试的元素组成的数组,如果没有任何数组元素通过测试,则返回空数组。)

callback

参数:

  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:boolean
  • 描述:判断元素是否通过测试

这个方法的命名也是一看就懂了,举个:chestnut::

  1. const test = [1, 2, 3, 4, 5];  
  2. const isMoreThanThree = (num) => { 
  3.   return num > 3; 
  4. }; 
  5.  
  6. const moreThanThreeList = test.filter(isMoreThanThree);  
  7. console.log(moreThanThreeList); // [4, 5] 

3.8 Array.prototype.reduce

reduce() 方法对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。

是否改变原数组: 否

参数:

  • callback:回调函数
  • initialValue:作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。
  • 返回值:函数累计处理的结果

callback

参数:

  • accumulator: 累计器累计回调的 返回值 ; 它是上一次调用回调时 返回的累积值 ,或initialValue
  • element:数组执行 callback 时的当前元素
  • index:当前元素的索引
  • array:调用方法的当前数组
  • 返回值:boolean
  • 描述:判断元素是否通过测试

这个方法,非常、非常强大,可以说几乎在任何 迭代数组,并得到一个返回值 的场景下,都可以适用。

比如:

  • 数学计算:累加、阶乘、求最大最小值......
  • 数组操作:去重、扁平化......
  • 函数式:pipe(从左往右执行函数组合)
  • 等等

这里我们举一个实现pipe的:chestnut::

  1. const pipe = (...fns) => { 
  2.   return (arg) => { 
  3.     return fns.reduce((res, fn) => { 
  4.       return fn(res); 
  5.     }, arg); 
  6.   }; 
  7. }; 

3.9 小结

不难发现,与数组迭代这类的方法,API都太相似了,而其实这些API语义化都是非常好的,多用用,自然就熟了。接着,不妨自己实现一个?

3.10 仿写 reduce

  1. Array.prototype.myReduce = function (fn, initialValue) { 
  2.   let ret = initialValue || this[0]; 
  3.   let idx = initialValue ? 0 : 1; 
  4.   while (idx < this.length) { 
  5.     ret = fn(ret, this[idx], idx, this); 
  6.     idx++; 
  7.   } 
  8.   return ret; 
  9. }; 
  10.  
  11. const test = [1, 2, 3, 4, 5]; 
  12.  
  13. const add = (num1, num2) => { 
  14.   return num1 + num2; 
  15. }; 
  16.  
  17. test.myReduce(add); // 15 
  18. test.myReduce(add, 10); // 25 

写的相对粗糙,其他的方法思路也是类似,有兴趣的同学可以自己写来看看,写完之后多少也能加深下理解。

4 其他方法

4.1 Array.prototype.flat

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

  • 是否改变原数组: 否
  • 参数:depth(指定要提取嵌套数组的结构深度,默认值为 1。)
  • 返回值:扁平化的数组(一个包含将数组与子数组中所有元素的新数组。)

曾经我一直认为扁平化这个方法在实际生产中用处不大。直到后来我维护的业务复杂起来后,需要用到 map 分类管理数据最后再取出来合并的时候,发现这方法还挺香的。举个:chestnut::

  1. const test = [1, [2, 3], [4, [5, 6]]];  
  2. test.flat(1); // [1, 2, 3, 4, [5, 6]]  
  3. test.flat(2); // [1, 2, 3, 4, 5, 6] 

自己实现个看看?其实我非常喜欢用栈来实现扁平化的思路,MDN上就有这个版本,但是我看相关文章提到这种方法的很少。

  1. const test = [1, [2, 3], [4, [5, 6]]]; 
  2.  
  3. const flatten = (list) => { 
  4.   const stack = [...list]; 
  5.   const ret = []; 
  6.   while (stack.length) { 
  7.     const topElem = stack.pop(); 
  8.     if (Array.isArray(topElem)) { 
  9.       stack.push(...topElem); 
  10.     } else { 
  11.       ret.push(topElem); 
  12.     } 
  13.   } 
  14.   return ret.reverse(); 
  15. }; 
  16.  
  17. flatten(test); // [1, 2, 3, 4, 5, 6] 

其实思路也是比较容易想到的:扁平化数组很明显是一个需要深度搜索的过程,而深度搜索需要用到栈,那么之后套栈的模版就行了。可能稍微需要转一下的就是最后反转数组这块。当然,如果按照完全符合常识来写,那么每一步入栈的是一个反转的元素就行了。

5. 结语

其实还是有不少方法也挺常用的,但是介于篇幅原因,这里就暂时先不展开了,比如:

  • Array.prototype.splice
  • Array.prototype.join
  • Array.prototype.concat

数组是开发中太常见的数据结构了,数组的方法一定要记熟才行。

其实我个人是不太喜欢写那种文式的文章的,但是写下来后还是不可避免的写成了那种类型的文章。本来这篇文章应该在两周前的周末发出来的,但是因为最近工作确实有些忙,就耽误了。坚持,真的不是一件容易的事情,写文章如此,学习亦是如此。

这两天半壁老师又发新歌了,在这安利一首半壁老师的《光明》吧。有些事情,是坚持下去才能看到希望的,但是在我看来,前端的学习其实真的没有那么困难,可能只是方法没对。希望大家都能坚持下去,找到自己远方的光明。

原文地址:https://juejin.cn/post/6924211360566312973?utm_source=tuicool&utm_medium=referral