数组的常规操作

177 阅读13分钟

遍历操作

for

for 循环是遍历数组最基本的方法

    const arr = [[0, 1, 2, 3, 4, 5, 6, 7]
    for (let i = 0; i < arr.length; i++) {
        if (i === 2) continue
        if (i === 5) break
        arr[i]++
    }
    console.log(arr) //[1, 2, 2, 4, 5, 5, 6,]
  • 当 i = 2 时,执行了 continue 操作,于是arr[2] = 2,没有加1
  • 当 i = 5时,break 跳出循环,所以后面的元素均未加1

由此可见 for中可以使用 continue 和 break,且支持改变原数组

forEach

forEach 支持数组元素的遍历,不支持break和continue,该函数返回值为undefined

    const arr = [1, 2, 3, 4, 5]

    arr.forEach(num => console.log(num));
    // [1, 2, 3, 4, 5]

使用 forEach 时需要注意的事项:

  1. forEach()方法不支持使用break或continue语句来跳出循环或跳过某一项。

  2. forEach 是一个同步方法,不支持异步处理,无法等待异步函数完成,它会继续执行下一项。这意味着如果在forEach()中使用异步函数,无法保证异步任务的执行顺序。

  3. 如果异步函数在执行时抛出错误,forEach()无法捕获该错误。

  4. forEach 删除自身元素,index不可被重置

  5. forEach 中 this 的指向问题,使用箭头函数的方式,或是使用bind对forEach进行绑定

  6. forEach性能比for循环低

    for:for循环没有额外的函数调用栈和上下文,所以它的实现最为简单。

    forEach:对于forEach来说,它的函数签名中包含了参数和上下文,所以性能会低于 for 循环。

  7. forEach使用不会改变原数组

for ... in

可以遍历对象,在遍历数组时每一项的 key 为下标值,

本质上该方法用来迭代对象,for...in 循环遍历对象的可枚举属性,包括对象自身的属性和继承的属性,由于数组是特殊的对象,也可以进行迭代,但不推荐。

  • 不建议使用for in 遍历数组,因为输出的顺序是不固定的,且会遍历数组的所有属性,包括数组的原型链上的属性。
  • 如果迭代的对象的变量值是null或者undefined, for in不执行循环体,
    // 遍历数组时 下标为 key
    const arr = [1,2,3,4]
     
    // for ... in 
    for (const key in arr){
        console.log(key) // 输出 0, 1, 2, 3
    }


    // 遍历对象时
    const object = { name: 'lx', age: 23 }
    // for ... in
    for (const key in object) {
      console.log(key) // 输出 name,age
      console.log(object[key]) // 输出 lx,23
    }

for ... of

for...of 不能遍历对象,只能遍历带有 iterator 迭代器的数据,例如:Set, Map, String, Array 类型

遍历数组时 key 为数组中的每一项值,且支持 continue、break 语句。

    // 遍历数组时 每一项的值为key
    const arr = [1,2,3,4]
     
    for (const key of arr){
        console.log(key) // 输出 1,2,3,4
    }

    //  遍历字符串
    const str ='abc'
    for(let char of str){
        console.log(char)   // a b c 
    }

    const a = ['A', 'B', 'C'];
    const s = new Set(['A', 'B', 'C']);
    const m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
    for (var x of a) { // 遍历Array
        alert(x);
    }
    for (var x of s) { // 遍历Set
        alert(x);
    }

    for (var x of m) { // 遍历Map
        console.log(x, x[0] + '=' + x[1]);
    }

for...in 与 for...of 的区别

  1. for...in 循环返回的是对象属性的名称(字符串类型),而 for...of 循环返回的是迭代对象的值。
  2. for...in 适合遍历对象,for...of 用于遍历数组

map

创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。

    const array1 = [1, 4, 9, 16];


    const map1 = array1.map(x => x * 2);

    [2, 8, 18, 32]

filter 过滤

遍历数组的同时根据条件找出所有符合条件的元素,并返回一个新的数组

cosnt arr = list.filter(item=>{
  return item.title.includes(str)
})

若无符合条件的数组则返回空数组

find

find()方法返回数组中符合条件的第一个元素

var stu = [
  {
    name: '张三',
    gender: '男',
    age: 20
  },
  {
    name: '王小毛',
    gender: '男',
    age: 20
  },
  {
    name: '李四',
    gender: '男',
    age: 20
  }
]
stu.find((element) => (element.name == '李四'))
//返回结果为
//{name: "李四", gender: "男", age: 20}

reduce

reduce() 方法接收一个函数作为累加器(accumulator),数组中的每个值(从左到右)开始缩减,最终为一个值。

var total = [0,1,2,3,4].reduce((a, b)=>a + b); //10

reduce接受一个函数,函数有四个参数,分别是:上一次的值,当前值,当前值的索引,当前数组

arr.reduce(callback,[initialValue])

  • callback (执行数组中每个值的函数,包含四个参数)
    • previousValue (上一次调用回调返回的值,或者是提供的初始值(initialValue))
    • currentValue (数组中当前被处理的元素)
    • index (当前元素在数组中的索引)
    • array (调用 reduce 的数组)
  • initialValue (初始值作为第一次调用 callback 的第一个参数。)

reduce() 对于空数组是不会执行回调函数的

    const arr = [1, 2, 3, 4, 5];
    const sum = arr.reduce(function(previousValue, currentValue, index, array){
      return previousValue + currentValue;
    })
    console.log(sum)  // 15

如果没有提供 initialValue,reduce 会从索引1的地方开始执行 callback 方法,将第一项作为初始值进行累加。如果提供initialValue,从索引0开始对初始值做累加。

some

some 对数组中每一项运行指定函数,返回一个布尔值,如果该函数对任一项返回 true,则停止遍历并返回 true。

    var arr = [ 1, 2, 3, 4, 5, 6 ]; 
       
    console.log( arr.some( function( item, index, array ){ 
      return item > 3; 
    })); 
    // true

只要当 arr 中有一个元素符合条件 item>3 就停止检测和遍历,并返回 true。

every

every对数组中的每一项运行给定函数,如果该函数对每一项返回 true,才会返回 true,若有一项不符合条件则立即停止遍历即可返回 false。

    var arr = [ 1, 2, 3, 4, 5, 6 ]; 

    console.log( arr.every( function( item, index, array ){ 
            return item > 3; 
        })); 
    // false

在给出的数组中并不是所有的值都大于3,在第一次经过比较后则立即被终止,并返回false

at

先来看看正常人是如何读取数组的最后一个元素:

const array = [ 0, 1, 2, 3, 4, 5 ]
const lastEle = array[ array.length - 1 ] // 5

除此之外还有还有别的办法吗?

当然还有啦 at将成为你的魔法😜

const array = [ 0, 1, 2, 3, 4, 5 ]
const lastEle = array.at(-1) // 5
const ele = array.at(0) // 0

通过 at 可以读取数组中下标所在位置的元素

常用 API

日常开发中数组常用API:

方法描述
push往数组末尾添加元素
pop从数组末尾删除元素
unshift往数组头部添加元素
shift从数组头部删除元素
concat连接2个或者更多数组,并返回结果
every对数组中的每一个元素运行给定的函数,如果每一个元素都返回true,则返回true
filter对数组中的每一个元素运行给定的函数,返回该函数会返回true的元素组成的数组
forEach对数组中的每一个元素运行给定的函数
join将所有的数组元素以指定的字符链接成一个字符串
indexOf返回第一个与给定参数相等的数组元素的索引,没有找到则返回-1
lastIndexOf从数组末尾开始搜索,返回第一个与给定参数相等的数组元素的索引,没有则返回-1
map对数组中的每一个元素运行给定的函数,返回每次函数调用的结果组成的数组
reverse颠倒数组中元素的顺序
slice传入索引值,将数组里对应索引范围内的元素作为新数组返回
some对数组中的每个元素运行给定的函数,如果任一元素返回true,则返回true
sort按照元素的ASCII值进行排序
reduce返回数组中所以元素值的合计
toString将数组作为字符串返回
valueOf和toString类似,将数组作为字符串返回
@@iterator返回一个包含数组键值对的迭代器对象,可以同步调用的方式得到数组元素的键值对
copyWhthin复制数组中的一系列元素到同一数组指定的起始位置
entries返回包含数组所有键值对的@@iterator
find根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素
findIndex根据回调函数给定的条件从数组中查找元素,如果找到则返回该元素的索引
fill用静态值填充数组
from将一个类数组转换为一个真正的数组
of根据传入的参数创建一个新数组
values返回包含数组中所以值的@@iterator
includes如果数组中存在某个元素,则返回true,否则返回false

sort

对数组进行排序,并返回,使得原数组发生变化

  • arr.sort((a,b)=>{ b-a}) 降序
  • arr.sort((a,b)=>{ a-b}) 升序

对数的排序,比较数值的大小进行排序:

    const arr = [34, 53, 12, 56, 32, 7, 35];

    // 一般写法
    arr.sort(function (a, b) {
        return a - b;   // 按照升序排列 
    });

    // 箭头函数
    arr.sort((a, b) => a - b);

    // [ 7, 12, 32, 34, 35, 53, 56]

在对字符串进行排序时会默认将元素转换为字符串,比较 utf-16 排序,无法保证时间复杂度 即 空间复杂度

当然同样适用于对象数组的排序: tempArr.sort((a, b) => b.value - a.value)

slice

slice(start, end) 截取数组,以新的数组对象,返回数组中被选中的元素

返回截取后的数组 [ start, end ) 不包括end处,且不改变原数组

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

slice 的使用

  1. 简单的复制实现数组的浅拷贝

没有任何参数的 slice 会执行一个简单的浅拷贝

  1. 抽取从 N 开始的数组
arr.slice(3)  // [4, 5]

3. 获取末尾 N 开始的数组

arr.slice(-3)  // [3, 4, 5]

4. 类数组的转换

通过 Array.prototype.slice.call(arguments) 将类数组转换为实际的数组

    function addOne() {
      // arguments 为函数调用时传入的参数,以一种类数组的形式存在
      return Array.prototype.slice.call(arguments).map(i => i+1)
    }
    /**
      [Arguments] {
        '0': 1,
        '1': 2,
        '2': 3,
      }
    **/
    addOne(1, 2, 3)   // [ 2, 3, 4 ]

类数组的本质为对象而并非是一个真实的数组,也被称为伪数组,它们具有数字索引和 length 属性,但不具备数组的所有方法。

  1. 将任意长度多余的参数强制转换为数组

将剩余参数组成数组,利用 Array.prototype.slice.call(arguments) 特性

    function myFunc(a, b) { 
      const extraArgs = Array.prototype.slice.call(arguments, 3); 
    }
    /**
      [Arguments] {
        '0': 1,
        '1': 2,
        '2': 3,
        '3': 4,
        '4': 5,
        '5': 6,
        '6': 7,
        '7': 8
      }
    **/
    myFunc(1, 2, 3, 4, 5, 6, 7, 8)

在函数里面会得到a == 1,b === 2,extraArgs=== [4, 5, 6, 7, 8 ]

splice

splice 能在任意位置添加、删除、替换元素,但是会改变原数组

  • splice( start, deleteCount, item1, item2)

    • start 开始的位置,想要删除或插入的元素的索引
    • deleteCount 删除的总个数 (若为0 则表示不删除元素)
    • item1,item2 为添加的元素 (从 start 位置前增加 item 元素)
    • 返回截取后的元素数组,原数组会发生变化
let numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.splice(0, 1)        // 在索引为0开始删除一个元素 [2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.splice(3, 2)        // 在索引为3的位置开始删除两个元素 [2, 3, 4, 7, 8, 9, 10]
numbers.splice(5, 0, 0, 1)  // 在索引为5前,添加0和1这两个元素 [2, 3, 4, 7, 8, 0,1,9, 10]

数组方法的手写实现

flat() 数组扁平化

flat()方法用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

例1:原生Array.prototype.flat方法接受一个depth参数,默认值为1,depth表示要降维的维数:

    const arr = [1, [2, 3], [4, [5, 6]]]
    console.log(arr.flat(1))         // [1, 2, 3, 4, [5, 6]]
    console.log(arr.flat(Infinity))  // [1, 2, 3, 4, 5, 6]

tips:再次熟悉一下 reduce 的用法

    let arr = [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]
    // 第一个参数
    //  prev: 上一次调用 callbackFn 时的返回值,用于保存累加。
    // 	在第一次调用时,若指定了初始值 initialValue,
    //  其值则为 initialValue,否则为数组索引为 0 的元素 array[0]
    //  val: 当前元素


    // 第二个参数:归并基础的初始值

    //  数组累加
    arr.reduce((prev,val)=>{
    	return prev + val
    })
    //25



    //  数组去重
    arr.resuce((prev,current)=>{
    	return prev.includes(current)? prev : prev.contact(current) 
    })

方法 一:reduce + 递归实现数组扁平化

    function flatReduce(list){
      return list.reduce((pre,next)=>{
        // Array.isArray 判断是否为数组
        return pre.concat(Array.isArray(next)?flatReduce(next):next)
      },[])
    }

方法二: 立即执行函数 + 循环递归实现数组扁平化

立即执行函数是闭包的一种实现,能获取外层的变量并进行操作

    function flatForEach(arr,depth){
      if(arr.length === 0 ){
        return []
      }
      let result = [];
      // 通过立即执行函数 可以实现闭包 通过操作外层的 result 实现扁平化 
      // 立即执行函书前的语句必须要有分号
      (function flatFunc(arr,depth){
        arr.forEach(item=>{
          if(Array.isArray(item) && depth >0) {
            // 递归实现扁平化
            flatFunc(item,depth-1)
          } else {
            result.push(item)
          }
        })
      })(arr,depth)
      return result
    }


    const arr2 = [1, 2,[ 3, [1, 2, 3, 4, [2, 3, 4]]]]
    const myResult3 = flatForEach(arr2, Infinity)
    console.log(myResult3 )     // [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

方法三:扩展运算符

    function myFlat(arr){
      while(arr.some(item=>Array.isArray(item))){
        arr = [].concat(...arr);
      }
      return arr;
    }

方法四:split 和 toString 共同处理

toString 会默认调用数组的 join() 将数组拼接成字符串,再使用 split 分割成数组

    function flatten(arr){
      return arr.toString().split(',')
    }
方法/问题实现难度编码思路
递归实现递归实现,返回新数组
reduce实现reduce进行累加操作
扩展运算符实现筛选出数组项进行连接
split和toString转成字符串再转数组
flat方法特定功能方法直接操作
正则和JSON方法JSON方法转成字符串转回过程中正则处理

数组去重

一: 通过 reduce 实现去重

    let arr = [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

    function deDuplicationArray(arr){
    	if(arr.length === 0 ){
        return []
      }
      return arr.reduce((prev,val)=>{
        //  不能使用 push 是因为 push() 返回的是数组的长度
        // return prev.includes(val) ? prev : prev.push(val)
        
        return prev.includes(val) ? prev : prev.concat(val)
      },[])
    }
    console.log(deDuplicationArray(arr))

二: set集合

Set是es6新增的数据结构,类数组,但它的一大特性就是所有元素都是唯一的,没有重复的值,我们一般称为集合。

    let arr = [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

    //  new Set 去重为类数组
    // 通过 Array.from() 转化为数组
    const arrSet = Array.from(new Set(arr))

循环中的异步处理

由于 forEach 不支持对异步操作的处理,所以 forEach 无法保证异步任务的顺序执行

如何在数组的循环过程中实现异步处理?

  1. 可以使用 map()、filter()、reduce()等,它们支持在函数中返回Promise,使用 Promise.all 并行所有的异步操作,Promise.all 会等待所有Promise完成。

    const arr = [1, 2, 3, 4, 5];

    async function asyncFunction(num) {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve(num * 2);
        }, 1000);
      });
    }

    const promises = arr.map(async (num) => {
      const result = await asyncFunction(num);
      return result;
    });

    Promise.all(promises).then((results) => {
      console.log(results); // [2, 4, 6, 8, 10]
    });

map 结合 promise 的使用,map()方法会等待异步函数完成并返回结果。

  1. 使用 for 循环来 串行 处理异步函数

    // 串行写法
    const forLoopAsync = async ()=>{
      for (const num of arr) {
        const m =  await asyncFunction(num)
        console.log(m);
      
      }
    }
    forLoopAsync()
  1. 改造forEach

由于 forEach 底层并没有实现异步的处理,才导致阻塞失效,那么对 forEach 进行改造使之支持异步 forEach

    async function forEach(arr,fn){
      for (let i = 0; i < arr.length; i++) {
        const item = arr[i];
        await fn(item, i, arr);
      }
    }


    forEach(arr, async item => {
      const m = await asyncFunction(item);
      console.log(m);
    })

参考

  1. Array.slice 8 种不同用法
  2. JS 的异步遍历,你真的会写吗?