常用数组方法整理

191 阅读17分钟

前言

最近写项目总是会处理一些很多的数据,而后端传过来的数据基本上都是数组的类型,所有避免不了我们去对这些数据进行处理,这里我列举了一些日常里常用的数组方法。

首先我们先来了解一下数组的一些常用方法

concat()  方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

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

findIndex()  方法返回数组中满足提供的测试函数的第一个元素的索引。否则返回-1。

indexOf()  方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1。 (通常用它判断数组中有没有这个元素)

includes() 方法用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回 true,否则返回false。

join() 方法将一个数组(或一个 类数组对象 ) 的所有元素连接成一个字符串并返回这个字符串。如果数组只有一个项目,那么将返回该项目而不使用分隔符。

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

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

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

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

splice() 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组

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

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

常用的数组循环方法

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

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

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

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

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

some() 方法测试是否至少有一个元素可以通过被提供的函数方法。该方法返回一个 Boolean 类型的值。

以上所有注解均来自 MDN

先来对常用的数组循环方法进行解刨

1、Array.forEach()

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

参数:forEach()接收两个参数

参数1:callback 为数组中每个元素执行的函数,该函数接收一至三个参数:

  1. item 数组中正在处理的当前元素
  2. index 数组中正在处理的当前元素的索引
  3. array forEach() 方法正在操作的数组

参数2:thisArg 可选

可选参数。当执行回调函数 callback 时,用作 this 的值。

返回值:undefined

问:forEach() 能直接修改原数组?

场景1

    const arr = [1, 2, 3]

    arr.forEach(item => {
      item = item + 1
      console.log(item);
    });

    console.log(arr);

打印

image.png

可以看到并没有修改原数组的元素

场景2

    const arr = [1, 2, 3]

    arr.forEach((item, index) => {
      arr[index] = item + 1
      console.log(item);
    });

    console.log(arr);

打印

image.png

回到问题:

可以看到forEach() 虽然无法直接修改原数组的元素,但forEach() 可以间接修改原数组的值,所以这个问题是个坑

面试回答:forEach 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。

问:forEach() 能否修改内部 this 的指向

场景1

    function Counter() {
      this.sum = 0;
      this.count = 0;
    }

    Counter.prototype.add = function (array) {
      array.forEach(function (entry) {
        console.log(this);
      })
    }
    
    const obj = new Counter();
    obj.add([1]);

打印

image.png

此时可以看到打印的是 Window

场景2

    function Counter() {
      this.sum = 0;
      this.count = 0;
    }
    Counter.prototype.add = function (array) {
      array.forEach(function (entry) {
        this.sum += entry;
        ++this.count;
        console.log(this);
      }, this);
    };

    const obj = new Counter();
    obj.add([2, 5, 9]);
    console.log(obj.count);
    // 3 === (1 + 1 + 1)
    console.log(obj.sum);
    16 === (2 + 5 + 9)

打印

image.png

这里我们在 forEach() 中添加第二个参数,打印的结果为 Counter 如果 thisArg 参数有值,则每次 callback 函数被调用时,this 都会指向 thisArg 参数。如果省略了 thisArg 参数,或者其值为 null 或 undefinedthis 则指向全局对象。按照函数观察到 this 的常用规则callback 函数最终可观察到 this 值。

2、Array.map()

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

参数:map()接收两个参数

参数1:callback 为数组中每个元素执行的函数,该函数接收一至三个参数:

  1. item 数组中正在处理的当前元素
  2. index 数组中正在处理的当前元素的索引
  3. array forEach() 方法正在操作的数组

参数2:thisArg 可选

可选参数。当执行回调函数 callback 时,用作 this 的值。

返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。

map() 的基本使用与 forEach 相同,只是 mpa 会返回一个新的数组

    const arr = ["1", "2", "3"];

    const newArr = arr.map(item => item = 'a')

    console.log(arr);
    console.log(newArr);

打印

image.png

可以看到他并没有修改原数组,会返回一个新数组

如果map 可以返回一个新对象,那么我们不传任何值 mpa 会返回新数组吗?

    const arr = ["1", "2", "3"];
    console.log(arr.amp());

来看打印

image.png

可以看到 map 的第一个参数的必传的,并且必须是函数

接下来我们来简单的实现一个map,来看看为什么 它可以返回一个新数组,而forEach不能

先来看 map

    const Arr = ["1", "2", "3"];

    // 在数组原型上挂载一个方法, Myap
    Array.prototype.Myap = function (fn) {
      // 先判断参数是否为 function
      if (typeof fn !== "function") {
        // 如果不是 function 则抛出一个 错误信息
        throw new TypeError(`${fn} is not a function`);
      }
      // 创建一个空数组
      let newArr = [];
      // 利用 for 循环, 加上数组中的 push
      // psuh 将传入的回调函数(fn)执行的返回值追加到空数组中
      for (let i = 0; i < this.length; i++) {
        // 将 this 中的每一项 当做实参, 传入 fn 中
        // 这里的 this 指向的是 arr
        // newArr.push(fn(this[i], i, this))
        newArr.push(fn(this[i], i, this))
        //     第一个参数 this[i] 也就是 数组的每一项
        //     第二个参数 i 也就是数组的 索引
        //     第三个参数 this 指的就是 原数组
      };
      // 之后返回 数组
      return newArr;
    }

测试结果

    const MyapArr = Arr.Myap((item, index, arr) => {
      console.log(item, index, arr);
      return item
    })
    console.log('Myap', MyapArr);

    console.log('---------------------');

    const mapArr = Arr.map((item, index, arr) => {
      console.log(item, index, arr);
      return item
    })
    console.log('map', mapArr);

打印

image.png

我们来看 forEach

    const Arr = ["1", "2", "3"];
    Array.prototype.for_Each = function (fn) {
      for (var i = 0; i < this.length; i++) {
        fn(this[i], i, this)
      }
    }

    console.log(Arr.for_Each((item, i, arr) => {
      item = 'a'
      console.log(item, i, arr);
    }));

测试结果

    console.log(Arr.for_Each((item, i, arr) => {
      item = 'a'
      console.log(item, i, arr);
    }));

    console.log('for_Each', Arr);

    console.log('---------------------');

    console.log(Arr.forEach((item, i, arr) => {
      item = 'a'
      console.log(item, i, arr);
    }));

    console.log('forEach', Arr);

打印

image.png

是不是与原方法完全一样。不过我们实现的也只是第一个参数,第二个参数感兴趣的可以去了解了解

3、Array.reduce()

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

我们先来看 reduce() 方法的基本使用

    const array = [1, 2, 3, 4];
    const reducer = (totalizer, item, index, arr) => totalizer + item;

    // 1 + 2 + 3 + 4
    console.log(array.reduce(reducer));

    // 5 + 1 + 2 + 3 + 4
    console.log(array.reduce(reducer, 5));

参数1:

callback 执行数组中每个值 (如果没有提供 totalizer 则第一个值除外)的函数,包含四个参数:

  1. totalizer 累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或 totalizer
  2. item 数组中正在处理的元素。
  3. index 数组中正在处理的当前元素的索引。 如果提供了initialValue,则起始索引号为0,否则从索引1起始。
  4. arr 调用reduce()的数组

参数2:

totalizer 作为第一次调用 callback函数时的第一个参数的值。 如果没有提供初始值,则将使用数组中的第一个元素。 在没有初始值的空数组上调用 reduce 将报错。

我们之直接上源码分析

    // 在数组的原型上挂载一个 MyReduce 方法 接收两个形参
    Array.prototype.MyReduce = function (reducer, initVal) {
      // 先判断 第一个 参数是否为 function
      if (typeof reducer !== "function") {
        // 如果不是 function 则抛出一个 错误信息
        throw new TypeError(`${reducer} is not a function`);
      }
      // 利用循环调用 reducer 并将返回值赋值给 initVal
      // 这样也就不难明白为啥, 需要一个累计器, 以及在 reducer 一定要有返回值
      for (let i = 0; i < this.length; i++) {
        initVal = reducer(initVal, this[i], i, this);
        // 这里的四个参数分别为
        //     第一个参数 initVal 累计器
        //     第二个参数 this[i] 也就是 数组的每一项
        //     第三个参数 i 也就是数组的 索引
        //     第四个参数 this 指的就是 原数组
      }
      // 最后返回 累计器 initVal
      return initVal
    };

测试结果

    array.MyReduce((acc, item, index, arr) => {
      console.log(acc, item, index, arr);
      return acc + item
    }, 0)

    console.log('---------------');

    array.reduce((acc, item, index, arr) => {
      console.log(acc, item, index, arr);
      return acc + item
    }, 0)

打印

image.png

这里总结一句

对于所有的循环方法的都 不会直接改变调用它的对象,但是那个对象可能会被 callback 函数改变。 every()filter()some() 这三个方法实现的逻辑很相似,与上面的三种方法原理差不多,感兴趣的可以的 MDN 了解了解

这里我们来对常用数组方法进行解刨

1、Array.concat()

01、 我们知道源数组调用 concat , 传入一个数组或者多个 它可以合并数组那,我们不传值或者 传入非数组类型的数据 Objectundefinednull ..... 会是什么?

    const array1 = ['a', 'b', 'c'];
    const array2 = ['d', 'e', 'f'];
    console.log(array1.concat());
    const obj = { a: '对象' }
    console.log(array1.concat(obj, undefined, null, 1, '2', () => { }));

我们来看打印结果

image.png

可以看到 当我们不传入任何值 会返回原数组 , 当传入非数组类型的数据 会作为数组的元素追加到原数组中。

02、concat() 的返回值 还是刚刚是数组我们来做测试

    const arr = array1.concat()
    arr[0] = 1
    console.log(arr);
    console.log(array1.concat());

打印结果

image.png

这里我们可以看到 concat 会返回一个新数组 并且不会对原数组造成影响

2、Array.find()

01、 它可以返回数组中满足条件的第一个元素,如果 都不满足则返回 undefined 这里就不多概述了,我们来看看他的参数,首先它接收一个实参,该实参为 函数 ,函数中有三个形参,分别为 当前遍历到的元素当前遍历到的索引数组本身

    const array = ['a', 'b', 'a'];
    array.find((item, index, arr) => {
      console.log(item, index, arr);
    })

打印结果

image.png

方法示例

    const array = ['a', 'b', 'a'];
    const res = array.find(item => item === 'a')
    console.log(res);

打印结果

image.png

测试

    const array = ['a', 'b', 'a', 'b'];
    const res = array.find(item => item = 'b')
    console.log(res);
    console.log(array);

这里我们返回一个 赋值操作来看打印结果

image.png

这里进行赋值操作他会返回数组的第一个值?(大神求解)

这里应该可以判断出该方法不会直接修改原数组值

02、 那么他的参数是必传的吗?如果该函数返回一个 undefined 会是什么?

    const array = ['a', 'b', 'a'];
    console.log(array.find(() => { }));
    console.log(array.find());

打印结果

image.png

可以看到该参数为必传不传则会报错,如果该函数返回一个 undefined 则结果也为 undefined

3、Array.findIndex()

01、 该方法与 上一个 find 有类似的效果,它会返回数组中满足条件的第一个元素的索引,否则返回 -1,该方法的参数与上一个一样这里不在概述。

这里我们直接来测试

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.findIndex(item => item === 'c')
    const res2 = array.findIndex(item => item === 'd')
    console.log(res1);
    console.log(res2);
    console.log(array);

打印结果

image.png

正常输出没有问题

来测试赋值操作

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.findIndex(item => item = 'c')
    console.log(res1);
    console.log(array);

打印结果

image.png

可以看到它会返回第一个元素索引,也不能直接修改原数组的值

4、Array.includes()

01、 该方法有两个参数

第一个参数 :需要查找的元素值 查看数组中是否包含该参数,如果有返回 true 否则 false

第二个参数 :为你要从第几个元素查找的索引值 , 如果传入一个负数、则按升序从 array.length + fromIndex 的索引开始搜 (即使从末尾开始往前跳 fromIndex 的绝对值个索引,然后往后搜寻)。默认为 0。

方法示例

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.includes('c', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

我们来看另一种情况

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.includes('b', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

打印居然为 falss ???

不慌我们来捋一捋

我们先想一想 第一次查找的是 c ,传入的索引为 -1 ,那问题来了我们是从第几个开始查找的呢?

在上面介绍过,如果传入的是一个负数,则按升序从 array.length + fromIndex 的索引开始搜

回到问题:数组 array 的 length 为 5,加上 -1 也就是从 索引为 4 的开始查找

正好 array[4] === c,而 在 c 的后面已经没有值了,所以 b 就找不到了

所以会返回 false

我们来测试一下

    const array = ['a', 'b', 'a', 'b', 'c', 'b'];
    const res1 = array.includes('b', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

可以看到我们的理论是正确的

5、Array.indexOf()

01、 该方法与 findIndex 有些许类似,findIndex 是返回第一个满足条件的的元素,接收的一个函数。indexOf 是返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1

方法示例

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.indexOf('a', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

同样来看另一种情况

    const array = ['a', 'b', 'a', 'b', 'c'];
    const res1 = array.indexOf('b', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

这里与 includes 的原理一样,我们之间看验证结果

    const array = ['a', 'b', 'a', 'b', 'c', 'b'];
    const res1 = array.indexOf('b', -1)
    console.log(res1);
    console.log(array);

打印结果

image.png

6、Array.join()

01、 简单的说他可以将数组以某个字符拼接成字符串。该方法如果不传入参数则会以逗号拼接 如果数组只有一个项目,那么将返回该项目而不使用分隔符

方法示例

    const array = ['a', 'b', 'a', 'b', 'c', 'b'];
    const res1 = array.join('=')
    console.log(res1);
    console.log(array);

打印结果

image.png

02、 如果数组中有复杂数据类型

    const array = ['a', 'b', 'a', 'b', 'c', 'b', {}];
    const res1 = array.join('=')
    console.log(res1);
    console.log(array);

打印结果

image.png

可以看到该参数是复杂数据类型是无法分隔的

7、Array.pop()

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

该方法会直接修改原数组

方法示例

    const array = ['a', 'b', 'c'];
    const res1 = array.pop()
    console.log(res1);
    console.log(array);

打印结果

image.png

02、 同样来看另一种场景

    const array = ['a', 'b', 'c'];
    const obj = {
      '0': '1',
      '1': '2',
      '2': '3',
      length: 3
    }
    console.log(array.pop.call(obj));
    console.log(obj);
    console.log(array);

打印结果

image.png

MDN 的描述

pop 方法从一个数组中删除并返回最后一个元素。

pop 方法有意具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。pop方法根据 length属性来确定最后一个元素的位置。如果不包含length属性或length属性不能被转成一个数值,会将length置为0,并返回undefined

如果你在一个空数组上调用 pop(),它返回 undefined

回到问题:

首先 popArray 原型上的方法, Object 类型的是无法调用的,cal() 方法是 Function 原型上的方法,array.pop.call(obj) 这里就是 数组调用 poppop 调用 call() ,这里通过 call 改变了 popthis 指向,将 popthis 指向了 obj

我们来看看 objarray 的内部长啥样

image.png

是不是知道为啥了吧,执行的都是一样的结构,所以 pop 才会执行成功

8、Array.shift()

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

从数组中删除的元素; 如果数组为空则返回 undefined

该方法会直接修改原数组

方法示例

    const array = ['a', 'b', 'c'];
    const res1 = array.shift()
    console.log(res1);
    console.log(array);

打印结果

image.png

02、 同样来看另一种场景

    const array = ['a', 'b', 'c'];
    const obj = {
      '0': '1',
      '1': '2',
      '2': '3',
      length: 3
    }
    const res1 = array.shift.call(obj)
    console.log(res1);
    console.log(obj);
    console.log(array);

打印结果

image.png

MDN 的描述

shift 方法并不局限于数组:这个方法能够通过 call 或 apply 方法作用于类似数组的对象上。但是对于没有 length 属性(从0开始的一系列连续的数字属性的最后一个)的对象,调用该方法可能没有任何意义。

原理与 pop 一样

9、Array.push()

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

该方法会直接修改原数组

方法示例

    const array = ['a', 'b', 'c'];
    const res1 = array.push('d', 'e')
    console.log(res1);
    console.log(array);

打印结果

image.png

02、 同样来看另一种场景

    const obj = {
      length: 0,

      addElem: function addElem(elem) {
        // obj.length 自动递增
        // 每次添加一个元素。
        [].push.call(this, elem);
      }
    };

    // 让我们添加一些空对象来说明。
    obj.addElem('a');
    obj.addElem({});
    obj.addElem({});
    console.log(obj.length);
    console.log(obj);

打印结果

image.png

MDN 的描述

push 方法具有通用性。该方法和 call() 或 apply() 一起使用时,可应用在类似数组的对象上。push 方法根据 length 属性来决定从哪里开始插入给定的值。如果 length 不能被转成一个数值,则插入的元素索引为 0,包括 length 不存在时。当 length 不存在时,将会创建它。

实现的原理与 pop 类似,这里我们可以看到他给对象obj添加了一个属性方法addElem,该方法通过数组身上的push方法通过call来调用改变 push 内部的 this 指向了该对象obj的属性方法addElem身上。所以当我们调用 obj.addElem() 向里面传值时,也就是调用了push函数内部的代码。

--图解--

image.png

10、Array.unshift()

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

该方法会直接修改原数组

方法示例

    const array = ['a', 'b', 'c'];

    const res = array.unshift('1', '2')
    console.log(res);
    console.log(array);

打印结果

image.png

02、 同样来看另一种场景

这里我通过 [].unshift.call() 来直接调用,并没有使用 push 方法描述中的把方法挂载到对象身上,其实原理都是一样的

    const obj = {
      '0': '1',
      '1': '2',
      '2': '3',
      length: 3
    }
    const res1 = [].unshift.call(obj, 'a', 'b', {})

    console.log(res1);
    console.log(obj);

打印结果

image.png

MDN 的描述

unshift 特意被设计成具有通用性;这个方法能够通过 call 或 apply 方法作用于类数组对象上。不过对于没有 length 属性(代表从0开始的一系列连续的数字属性的最后一个)的对象,调用该方法可能没有任何意义。

从这我们可以得出 unshiftpush 实现的原理是一样的

11、Array.splice()

01、 splice 方法通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。此方法会改变原数组。

该方法会直接修改原数组

该方法接受三个参数:

  1. start指定修改的开始位置
    • 指定修改的开始位置(从0计数)。如果超出了数组的长度,则从数组末尾开始添加内容;如果是负值,则表示从数组末位开始的第几位(从-1计数,这意味着-n是倒数第n个元素并且等价于array.length-n);如果负数的绝对值大于数组的长度,则表示开始位置为第0位。
  2. deleteCount整数,表示要移除的数组元素的个数
    • 如果 deleteCount 大于 start 之后的元素的总数,则从 start 后面的元素都将被删除(含第 start 位)。

    • 如果 deleteCount 被省略了,或者它的值大于等于array.length - start(也就是说,如果它大于或者等于start之后的所有元素的数量),那么start之后数组的所有元素都会被删除。

    • 如果 deleteCount 是 0 或者负数,则不移除元素。这种情况下,至少应添加一个新元素。

  3. item1, item2, ...
    • 要添加进数组的元素,从start 位置开始。如果不指定,则 splice() 将只删除数组元素

返回值:

由被删除的元素组成的一个数组。如果只删除了一个元素,则返回只包含一个元素的数组。如果没有删除元素,则返回空数组。

代码实例

    const array = ['a', 'b', 'c'];

    const res = array.splice(0, 2)
    console.log(res);
    console.log(array);

打印结果

image.png

02、 同样来看另一种场景

场景1

    const array = ['a', 'b', 'c'];

    const res = array.splice(0, 2, 'd')
    console.log(res);
    console.log(array);

打印结果

image.png

这里首先是把原数索引为 0 开始删除了 2 个,然后在原数组的第 0 项添加了 d

场景2

    const array1 = ['a', 'b', 'c'];
    const array2 = ['d', 'e', 'f'];

    const res1 = array1.splice(-1, 2, '新增的1')
    const res2 = array2.splice(-10, 2, '新增的2')

    console.log(res1);
    console.log(array1);
    console.log('--------------------');
    console.log(res2);
    console.log(array2);

打印结果

image.png

res1 打印

当指定修改位置为 负数时候,则表示从数组末位开始的第几位(也就是倒数第一位),所以会从最后一个 ‘c’ 开始删除 ,删除 2个 ,应为 c 已经是最后一个元素了 所以只删除了一个,之后在倒数第一位的位置添加了 一个新元素 新增的1

res2 打印

这里可以看到,当前指定修改的位置的值为 -10 ,根据 res1 打印的理论那么应该从 倒数第 10 为开始删除,但原数组的长度只有 3 个 ,所以这里就直接从第 0 个开始删除了 ,同样在第 0 位添加了一个元素 新增的2

12、Array.reveres()

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

该方法会直接修改原数组

方法示例

    const array = [1, 2, 3, 4, 5]
    const res = array.reverse()
    console.log(res);
    console.log(array);

打印结果

image.png

返回值:返回翻转后的数组