你需要知道的JS数组

1,876 阅读11分钟

数组作为JS的基础,是每个前端程序员都需要掌握的基础。本人作为前端小白,不管是刚起步的时候,还是现在接触项目,或者练习简单的数组算法题的时候,就一看到代码或题目,脑子里经常只会想起for循环,逻辑晦涩难懂,代码庞大冗余,上手及其困难。很大的原因就是数组方法使用的不熟练,有时候一大串的代码,只需要借用数组的方法再加以改动就可以变得简单高效又优雅。因此我在这里总结下数组的常用方法

那么数组操作,我们首先需要注意并且牢记的是splice、sort、reverse 这3个常用方法是对数组自身的操作,会改变数组自身

数组常用方法

  • 添加/删除元素:

    • push(...items): 从结尾添加元素,并返回新的长度
    • pop( ): 用于删除数组的最后一个元素,并返回删除的元素
    • shift( ): 从开头删除元素,并返回第一个元素的值
    • unshift(...items):从开头添加元素,并返回新的长度
    • splice(pos,deleteCount,...items):从index开始删除,删除deleteCount元素,并在当前位置插入一个或者多个元素,如果只是删除了元素,则返回的是含有被删除的元素的数组,否则为空
    • slice(start,end):它从索引“start”复制到“end”(不包括“end”)返回一个新的数组
    • concat(...items):返回一个新数组:复制当前数组的所有成员并向所有成员中添加items,如果有任何items是一个数组,那么久取其全部元素
  • 查询元素:

    • indexOf(item,pos):从pos位置开始检索item,有就返回索引否则返回-1;如果没有设置pos则默认从0开始
    • lastIndexOf(item,pos):从pos位置开始检索item,有就返回索引否则返回-1;如果没有设置pos则默认从length-1开始
    • includes(value):如果数组有value,则返回true,否则返回false
    • find/filter(func):通过函数过滤元素,返回符合find函数的第一个值或者符合filter函数的全部值
    • findIndex(func):和find类似,但是它返回索引而不是值
  • 转换数组:

    • map(func): 从每个元素调用func的结果并返回一个新数组
    • sort(func): 将数组排序,如何从小到大,func((a,b) => a-b),否则b-a,原地修改数组,并返回修改后的新数组
    • reserve():在原地颠倒数组,然后返回它
    • split/join:前者将字符串转换为数组,后者将数组转换为字符串,并返回
    • reduce(func,initial):一般用于求和,通过为每个元素调用func计算数组上的单个值并在调用之间传递中间结果
  • 迭代元素

    • forEach(func): 为每个元素调用func,不返回任何东西
  • 其他

    • Array.isArray(arr): 检查arr是否是一个数组
    • arr.some(fn)/arr.every(fn): 在类似于map的数组的每个元素上调用函数fn。如果任何/所有结果为true,则返回true,否则返回false。常用于检查元素
    • arr.fill(value,start,end): 从start到end用value 重复填充数组
    • arr.copyWithin(target,start,end):将在tartget位置索引开始,从start到end复制到本身例如:
      var fruits = ["Banana", "Orange", "Apple", "Mango"];
      fruits.copyWithin(2, 0);
      // fruits
      Banana,Orange,Banana,Orange
      

数组去重

1.利用ES6 Set去重

function unique (arr) {
  return [...new Set(arr)] // Array.from(new Set(arr))
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr))
 //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

不考虑兼容性,这种去重的方法代码最少。这种方法还无法去掉“{}”空对象,后面的高阶方法会添加去掉重复“{}”的方法。

为什么无法去掉"{}"对象呢?

原因在于set方法比较时会比较key和value,前者使用hashCode() 后者使用equals()方法,如果set对象不手动使用equals()和hashCode(),key值是不同的,所以无法识别去重

2.利用for嵌套for,然后splice去重(ES5中最常用)

双层循环,外层循环元素,内层循环时比较值。值相同时,则删去这个值。

function unique(arr){            
        for(var i=0; i<arr.length; i++){
            for(var j=i+1; j<arr.length; j++){
                if(arr[i]===arr[j]){         //第一个等同于第二个,splice方法删除第二个
                    arr.splice(j,1);
                    j--;
                }
            }
        }
return arr;
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr));
    //[1,'true',true, 15,false, undefined,null,NaN,NaN,'NaN',0,'a',{},{}];
    //NaN和{}没有去重

NaN和{}没有去重

为什么 NaN === NaN 为false

NaN的意思是Not a Number,那么不是数字的字符肯定不是一个,而是一个范围,一个集合。就好像A不是数字,B也不是数字,但是A肯定不是B一样。所以综上NaN其实是不等于它自身的。

为什么 {} === {} 为false

当比较两个object大小时,会调用object的valueOf方法,如果结果能直接比较,则返回比较结果;如果结果不能直接比较大小,则会调用object的toString方法,并返回比较结果

3.利用indexOf去重

新建一个空的结果数组,for循环原数组,判断结果数组是否存在当前元素,如果有相同的值则跳过,不相同则push进数组。

var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];

function unique(arr) {
  let newArr = [];
  for (let i = 0; i < arr.length; i++) {
    if(newArr.indexOf(arr[i]) === -1) {
      newArr.push(arr[i])
    }
  }
  return newArr
}
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {…}, {…}] 

NaN、{}没有去重

4.利用sort()

function unique(arr) {
    arr = arr.sort()
    var arrry= [arr[0]];
    for (var i = 1; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            arrry.push(arr[i]);
        }
    }
    return arrry;
}
     var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
// [0, 1, 15, "NaN", NaN, NaN, {…}, {…}, "a", false, null, true, "true", undefined]      

NaN、{}没有去重

5.利用includes

function unique(arr) {
    var array =[];
    for(var i = 0; i < arr.length; i++) {
            if( !array.includes( arr[i]) ) {//includes 检测数组是否有某个值
                    array.push(arr[i]);
              }
    }
    return array
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
    console.log(unique(arr))
    //[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {…}, {…}]   

{}没有去重

6.利用hasOwnProperty

obj.hasOwnProperty(prop) hasOwnProperty( )方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)

参数

prop:要检测的属性的 String 字符串形式表示的名称,或者 Symbol。

返回值

用来判断某个对象是否含有指定的属性的布尔值 Boolean。

即使属性的值是 null 或 undefined,只要属性存在,hasOwnProperty 依旧会返回 true。

利用hasOwnProperty 判断是否存在对象属性

function unique(arr) {
  var obj = {}
  return arr.filter(function(item,index,arr){
    return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true)
  })
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}]

所有的都去重了

7.利用filter

function unique(arr) {
  return arr.filter(function(item,index,arr){
    return arr.indexOf(item) === index;
  })
}

var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
//[1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {}, {}]

{}没有去重

8.利用reduce+includes

function unique(arr) {
  return arr.reduce((pre,cur) => pre.includes(cur) ? pre : [...pre,cur],[])
}
var arr = [1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}];
console.log(unique(arr));
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]

生成类似[1-100]这样的数组

测试大量数据的数组时可以这样生成:

const arr = new Array(100).fill(0).map((item,index) => index + 1) 

new Array(100) 会生成一个有100空位的数组,这个数组是不能被map(),filter(),reduce(),every(),some()遍历的,因为空位会被跳过(for of不会跳过空位,可以遍历)。可以通过[...new Array(100)]给空位设置默认值undefined,从而使数组可以被以上方法遍历

数组取交集

const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const duplicatedValues = [...new Set(a)].filter(item => b.includes(item))
duplicatedValues // [3, 4, 5]

数组取差集

const a = [0, 1, 2, 3, 4, 5]
const b = [3, 4, 5, 6, 7, 8]
const diffValues = [...new Set([...a, ...b])].filter(item => !b.includes(item) || !a.includes(item)) // [0, 1, 2, 6, 7, 8]

数组转对象

const arr = [1,2,3,4]
const obj = {...arr} // {0: 1, 1: 2, 2: 3, 3: 4}
const newObj = {0: 0, 1: 1, 2: 2, length: 3}
// 对象转数组不能用展开运算符,因为它必须用在可迭代对象上
const newArr= [...newObj] // Uncaught TypeError: object is not iterable...
// 可以使用Array.form()将类数组对象转为数组
let newArr = Array.from(newObj) // [0, 1, 2]

什么是可迭代对象?

遍历Array可以采用下标循环,遍历Map和Set就无法使用下标。为了统一集合类型,ES6标准引入了新的iterable类型,Array、Map和Set都属于iterable类型。

具有iterable类型的集合可以通过新的for ... of循环来遍历。

for ... of循环是ES6引入的新的语法,可以测试自己的浏览器是否支持:

'use strict';
var a = [1, 2, 3];
for (var x of a) {
}
console.log('你的浏览器支持for ... of');

那么for ... of循环和for ... in循环有何区别? for ... in循环由于历史遗留问题,它遍历的实际上是对象的属性名称。一个Arrat数组实际上也是一个对象,它的每个元素的索引被视为一个属性。

当我们手动给Array对象添加了额外的属性后,for ... in循环将带来意想不到的意外效果:

var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x in a) {
    console.log(x); // '0', '1', '2', 'name'
}

for ... in 循环将把name包括在内,但Array的length属性却不包括在内。 for ... of 循环则完全修复了这些问题,它只循环集合本身的元素:

var a = ['A', 'B', 'C'];
a.name = 'Hello';
for (var x of a) {
    console.log(x); // 'A', 'B', 'C'
}

这就是为什么要引入新的for ... of 循环

然而,更好的方式是直接使用iterable内置的forEach方法,它接收一个函数,每次迭代就自动回调该函数。 注意,forEach()方法是ES5.1标准引入的,你需要测试浏览器是否支持。

Array.from()方法

在了解Array.from()方法之前,我们先来介绍一下类数组对象

  • 类数组对象:所谓类数组对象,最基本的要求就是具有length属性的对象。

  • Array.from():Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组。

  • Array.from有三个参数,Array.from(object, mapFnc, thisValue),该方法的返回值是一个新的数组实例(真正的数组)

    • object:是必需的,想要转换成数组的伪数组对象或可迭代对象;
    • mapFnc:可选,新数组中每个元素都会执行该回调函数
    • thisValue: 可选,执行回调函数mapFnc时this对象
  • 要将一个类数组对象转换为一个真正的数组,必需具备以下条件:

    • 该类数组对象必须具有length属性,用于指定数组的长度。如果没有length属性,那么转换后的数组是一个空数组。
    • 该类数组对象的属性名必需为数值型或字符串型的数组,该类数组对象的属性名可以加引号,也可以不加引号。
  • 可以将Set解构的数据转换为数组

let arr = [1,2,3,4,5,6,7,8,9]
let set = new Set(arr)
console.log(Array.from(set))  // [1,2,3,4,5,6,7,8,9]

数组常用遍历

数组常用遍历有 forEach、every、some、filter、map、reduce、reduceRight、find、findIndex 等方法,很多方法都可以达到同样的效果。数组方法不仅要会用,而且要用好。要用好就要知道什么时候用什么方法。

遍历的混合使用

filter、map方法返回值仍是一个数组,所以可以搭配其他数组遍历方法混合使用。注意遍历越多效率越低~

const arr = [1, 2, 3, 4, 5]
const value = arr
    .map(item => item * 3) // [3, 6, 9, 12, 15]
    .filter(item => item % 2 === 0) // [6, 12]
    .map(item => item + 1) // [7, 13]
    .reduce((prev, curr) => prev + curr, 0) // [20]
console.log(value); // 20

检测数组所有元素是否都符合判断条件

const arr = [1, 2, 3, 4, 5]
const isAllNum = arr.every(item => typeof item === 'number')
console.log(isAllNum); // true

检测数组是否有元素符合判断条件

const arr = [1, 2, 3, 4, 5]
const hasNum = arr.every(item => typeof item === 'number')
console.log(hasNum); // true

找到第一个符合条件的元素/下标

const arr = [1, 2, 3, 4, 5]
const findItem = arr.find(item => item === 3) // 返回子项3
const findIndex = arr.findIndex(item => item === 3) // 返回子项的下标2


数组使用误区

数组的方法很多,很多方法都可以达到同样的效果,所以在使用时要根据需求使用合适的方法。

垃圾代码产生的很大原因就是数组常用方法使用不当,这里有以下需要注意的点:

array.includes() 和 array.indexOf()

array.includes() 返回布尔值,array.indexOf() 返回数组子项的索引。indexOf 一定要在需要索引值的情况下使用。

注意:array.indexOf()找 NaN 会找不到,返回-1,array.includes()能找到,返回true~

[NaN].includes(NaN) // true
[NaN].indexOf(NaN) // -1

array.find() 和 array.filter()

array.filter() 返回的是所有符合条件的子项组成的数组,会遍历所有数组; 而 array.find() 只返回第一个符合条件的子项,是短路操作