JS 数组常见操作汇总,数组去重、降维、排序、多数组合并实现思路整理 随笔 第1张

壹 引

JavaScript开发中数组加工极为常见,其次在面试中被问及的概率也特别高,一直想整理一篇关于数组常见操作的文章,本文也算了却心愿了。

说在前面,文中的实现并非最佳,实现虽然有很多种,但我觉得大家至少应该掌握一种,这样在面试能解决大部分数组问题。在了解实现思路后,日常开发中结合实际场景优化实现,提升性能也是后期该考虑的。

SRE实战 互联网时代守护先锋,助力企业售后服务体系运筹帷幄!一键直达领取阿里云限量特价优惠。

本文主要围绕数组去重、数组排序、数组降维、数组合并、数组过滤、数组求差集,并集,交集,数组是否包含某项等知识点展开,附带部分知识拓展,在看实现代码前也建议大家先自行思考,那么本文开始。

贰 常见数组操作

贰 壹 数组去重

数组去重我分为两种情况,简单数组去重与对象数组去重。所谓简单数组即元素均为基本数据类型,如下:

let arr = [undefined, 0, 1, 2, 2, 3, 4, 0, undefined];
let arr_ = arr.filter((self, index, arr) => index === arr.indexOf(self));
console.log(arr_); //[undefined, 0, 1, 2, 3, 4]

有没有更简单的做法?有的同学肯定想到了ES6新增的Set数据结构,这也是去重的妙招,原理是Set结构不接受重复值,如下:

[...new Set([undefined, 0, 1, 2, 2, 3, 4, 0, undefined])]//[undefined, 0, 1, 2, 3, 4]

对象数组顾名思义,每个元素都是一个对象,比如我们希望去除掉name属性相同的对象:

let arr = [{name:'echo'},{name:'听风是风'},{name:'echo'},{name:'时间跳跃'}];
let keys = {};
let arr_ = arr.reduce((accumulator,currentValue)=>{
     !keys[currentValue['name']] ?
      keys[currentValue['name']] = true && accumulator.push(currentValue) :
      null;
     return accumulator;
},[]);
console.log(arr_);//[{name:'echo'},{name:'听风是风'},{name:'时间跳跃'}]

思路并不难,我们借助一个空对象keys,将每次出现过的对象的name值作为key,并将其设置为true;那么下次出现时根据三元判断自然会跳过push操作,从而达到去重目的。

reduce存在一定兼容问题,至少完全不兼容IE,不过我们知道了这个思路,即使使用forEach同样能做到上面的效果,改写就留给大家了。

有同学肯定就想到了,能不能使用Set去重对象数组呢?其实并不能,因为对于JavaScript来说,两个长得相同的对象只是外观相同,它们的引用地址并不同,比如:

[1,2,3]===[1,2,3]//false

所以对于Set结构而言,它们就是不同的两个值,比如下面这个例子:

[...new Set([{name:'echo'},{name:'echo'}])]//{name:'echo'},{name:'echo'}

浅拷贝可以让两个对象完全相等,如下:

let a=[1,2];
let b = a;
console.log(a===b);//true

所以我们可以用new Set()去重引用地址相同的对象:

let a = {name:'echo'};
let b = a;
console.log([...new Set([a,b])]); //{name: "echo"}

大概这么个意思,关于数组去重先说到这。

贰 贰 数组降维

数组降维什么意思?举个例子,将二维数组[[1,2],[3,4]]转变为一维数组[1,2,3,4 ]

ES6中新增了数组降维方法flat,使用比较简单,比如就上面的例子可以这么做:

let arr = [[1,2],[3,4]];
let arr_ = arr.flat();
console.log(arr_);//[1, 2, 3, 4]

如果是三维数组怎么办呢?falt方法接受一个参数表示降维的层数,默认为1,你可以理解为要去掉 [] 的层数。

三维数组降维可以这么写:

let arr = [[1,2],[3,4],[5,[6]]];
let arr_ = arr.flat(2);
console.log(arr_);//[1, 2, 3, 4, 5, 6]

如果你不知道数组要降维的层数,你可以直接将参数设置为infinity(无限大),这样不管你是几维都会被降为一维数组:

let arr = [[[[[1,2]]]]];
let arr_ = arr.flat(Infinity);
console.log(arr_);//[1, 2]

简单粗暴,好用是好用,兼容也是个大问题,谷歌版本从69才完全支持,其它浏览器自然没得说。

我们可以简单模拟flat实现,如下:

let arr = [0, [1],
    [2, 3],
    [4, [5, 6, 7]]
];

function flat_(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('The argument must be an array.');
    };
    let arr_ = [];
    arr.forEach((self) => {
        Array.isArray(self) ?
            arr_.push.apply(arr_, flat_(self)) :
            arr_.push(self);
    });
    return arr_;
};
flat_(arr); //[0, 1, 2, 3, 4, 5, 6, 7]

在这个实现中,巧妙使用apply参数接受数组的特点,让push也能扁平化接受一个一维数组,从而达到数组合并的目的。

换种思路,使用reduce结合concat方法,实现可以更简单一点点,如下:

function flat_(arr) {
    if (!Array.isArray(arr)) {
        throw new Error('The argument must be an array.');
    };
    return arr.reduce((accumulator, currentValue) => {
        return accumulator.concat(Array.isArray(currentValue) ? flat_(currentValue) : currentValue);
    }, []);
};
console.log(flat_(arr));//[0, 1, 2, 3, 4, 5, 6, 7]

这个实现也只是省略了创建新数组与返回新数组两行代码,这两个操作reduce都帮我们做了。

实现一依赖的是push,实现二依赖的是concat,同为数组方法,这里说几个大家容易忽略的知识点。

concat除了能合并数组,其实也能合并简单类型数据,实现二中正是利用了这一点:

[1,2,3].concat([4]);//[1,2,3,4]
[1,2,3].concat(4);//[1,2,3,4]

concat返回合并后的新数组,而push返回添加操作后数组的长度

let a = [1,2,3].concat([4]);
console.log(a);//[1,2,3,4]
let b = [1,2,3].push(4);
console.log(b);//4

concat属于浅拷贝,这是很多人都容易误解的一个点,一个误解的例子:

let arr = [1,2,3];
let a = arr.concat();
arr[0] = 0;
console.log(a);//[1, 2, 3]

而在下面这个例子中,你会发现concat确实是浅拷贝:

let arr_ = [[1,2],[3]];
let a_ = arr_.concat();
arr_[0][0] = 0;
console.log(a_);//[[0,2],[3]]

这是为什么?在MDN文档说明中解释的很清楚,concat创建一个新数组,新数组由被调用的数组元素组成,且元素顺序与原数组保持一致。元素复制操作中分为基本类型与引用类型两种情况:

数据类型如字符串,数字和布尔(不是StringNumberBoolean 对象):concat将字符串和数字的值复制到新数组中。

对象引用(而不是实际对象):concat将对象引用复制到新数组中。 原始数组和新数组都引用相同的对象。 也就是说,如果引用的对象被修改,则更改对于新数组和原始数组都是可见的。 这包括也是数组的数组参数的元素。

有人觉得concat是深拷贝,也是因为数组中的元素恰好是基本数据类型,这点希望大家谨记。那么关于数组降维就说到这里了。

贰 叁 数组合并、多数组合并

在介绍数组降维时我们顺带提及了数组合并的一些做法,如果只是合并两个数组我们可以这样做:

let arr1 = [1, 2];
let arr2 = [3, 4];
arr1.concat(arr2); //[1,2,3,4]

arr1.push.apply(arr1, arr2);
arr1; //[1,2,3,4]

Array.prototype.concat.apply(arr1, arr2); //[1,2,3,4]

那如果是未知个数的数组需要合并怎么做呢?使用ES6写法非常简单:

let arr1 = [1, 2],
    arr2 = [3, 4],
    arr3 = [5, 6];

function concat_(...rest) {
    return [...rest].flat();
};
concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]

这里一共只做了两件事,使用函数rest参数配合拓展运算符...将三个数组组成成一个二维数组,再利用flat降维。

当然考虑兼容问题,我们可以保守一点这么去写:

let arr1 = [1, 2],
    arr2 = [3, 4],
    arr3 = [5, 6];

function concat_() {
    let arr_ = Array.prototype.slice.call(arguments);
    let result = [];
    arr_.forEach(self => {
        result.push.apply(result, self);
    });
    return result;
};
concat_(arr1, arr2, arr3); //[1, 2, 3, 4, 5, 6]

有同学一定在想,为什么forEach内不直接使用result.concat(self)解决合并呢?原因有两点:

  • concat不修改原数组而是返回一个新数组,所以循环多次result还是空数组。

  • forEach不支持return,无法将合并过的数组返回供下次继续合并,这两个问题使用reduce都能解决。

贰 肆 数组排序

这个自然不用说了,我想大家首先想到的自然是sort排序,直接上代码:

//升序
[1, 0, 2, 5, 4, 3].sort((a, b) => a - b); //[0,1,2,3,4,5]
//降序
[1, 0, 2, 5, 4, 3].sort((a, b) => b - a); //[5,4,3,2,1,0]

那么问题就来了,虽然我们知道sort是按字符编码的顺序进行排序,那么上述代码中的回调函数起到了什么作用?其实这一点在JavaScript权威指南中给出了答案:

若想让sort按照其它方式而非字母表顺序进行数组排序,必须给sort方法传递一个比较函数。该函数决定了它的两个参数在排好序的数组中的先后顺序,假设第一个参数应该在前,比较函数应该返回一个小于0的数值;相反,假设第一个参数应该在后,函数应该返回一个大于0的数值。并且,假设两个值相等,函数应该返回0;

什么意思呢?以上面的a - b为例,因为ab均为数字,所以计算结果只能是正数,0,负数三种情况,如果为负数则a排在b前面,如果相等,ab顺序不变,如果为正数,a排在b后面,大概这个意思。

我们将问题升级,现在需要按照年龄从小到大对用户进行排序,可以这么做:

var arr = [{
    name: 'echo',
    age: 18
}, {
    name: '听风是风',
    age: 26
}, {
    name: '时间跳跃',
    age: 10
}, {
    name: '行星飞行',
    age: 16
}];
arr.sort((a, b) => {
    var a_ = a.age;
    var b_ = b.age;
    return a_ - b_;
});

比较巧的是上面2个例子参与比较的元素都为数字,所以能参与计算比较,前面已经说了sort方法默认是按照字符编码的顺序进行排序:

['c', 'b', 'a', 'e', 'd'].sort();//["a", "b", "c", "d", "e"]

现在要求以上字母按z-a倒序排列,怎么做?虽然字母无法计算,但还是有大小之分,还是一样的做法,如下:

['c', 'b', 'a', 'e', 'd'].sort((a, b) => {
    let result;
    if (a < b) {
        result = 1;
    } else if (a > b) {
        result = -1;
    } else {
        result = 0;
    };
    return result;
}); //["e", "d", "c", "b", "a"]

在介绍sort回调含义的时候已有解释,若希望从小到大排列,a<b应该返回小于0的数字,但我们希望排序是由大到小,所以反过来就可以了,让a<b时返回大于0的数字,a>b返回小于0的数字,这样就可以实现倒序排列。

我知道,关于排序大家都有听过冒泡、插入等十大经典排序算法,因为篇幅问题这里就不贴代码了,如果时间允许我会专门写一篇简单易懂的十大排序的文章,那么关于排序就说到这里了。

贰 伍 数组过滤

数组过滤在开发中即为常见,我们一般遇到两种情况,一是将符合条件的元素筛选出来,包含在一个新数组中供后续使用;二是将符合条件的元素从原数组中剔除。

我们先说说第一种情况,筛选符合条件的元素,实现很多种,首推filter,正如单词含义一样用于过滤:

// 筛选3的倍数
[1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => self % 3 === 0);//[3,6,9]

第二种删除符合条件的元素,这里可以使用for循环:

// 剔除3的倍数
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9],
    i = 0,
    length = arr.length;

for (; i < length; i++) {
    // 删除数组中所有的1
    if (arr[i] % 3 === 0) {
        arr.splice(i, 1);
        //重置i,否则i会跳一位
        i--;
    };
};
console.log(arr);//[1, 2, 4, 5, 7, 8]

我们换种思路,剔除数组中3的倍数不就是在找不是3的倍数的元素吗,所以还是可以使用filter做到这一点:

[1, 2, 3, 4, 5, 6, 7, 8, 9].filter(self => !(self % 3 === 0));

有同学肯定纳闷为什么不用forEach做呢?这是因为forEach不像for循环能重置i一样重置index,其次不像filter能return数据,对于forEach使用更多细节可以阅读博主这篇文章 forEach参数详解,forEach与for循环区别 。那么关于数组过滤就说到这里了。

贰 陆 判断数据是否包含某元素

同为高频操作,很多同学习惯使用for或者forEach用来做此操作,其实相比之下,find与some方法更为实现,先看find:

var result = ['echo', '听风是风', '时间跳跃', '听风是风'].find((self) => {
    console.log(1);//执行2次
    return self === '听风是风'
});
console.log(result); //听风是风

再看some方法:

var result = ['echo', '听风是风', '时间跳跃'].some((self) => {
    console.log(1);//执行2次
    return self === '听风是风'
});
console.log(result); //true

find方法返回第一个符合条件的目标元素,并跳出循环,而some只要找到有一个符合条件则返回布尔值true。两者都自带跳出循环机制,相比for循环使用break以及forEach无法break更加方便,特别是some的返回结果更利于后面的条件判断逻辑。

另外ES6数组新增了简单粗暴的includes方法,能直接用于判断数组是否包含某元素,最大亮点就是能判断是否包含NaN,毕竟大家都知道NaN是唯一不等于自己的特殊存在。

[1,2,3,NaN].includes(NaN);//true

includes方法完全不兼容IE,这里只是顺带一提,实际开发中还得谨慎使用。

贰 柒 数组求并集、交集、差集

在说实现之前,我们简单复习数学中关于并集,交集与差集的概念。

假设现在有数组A [1,2,3]与数组B [3,4,5],因为3在两个数组中均有出现,所以3是数组AB的交集。

那么对应的数字1,2只在A中存在,4,5只在B中出现,所以1,2,3,4属于AB的共同差集。

而并集则是指分别出现在AB中的所有数字,但不记重复,所以是1,2,3,4,5,注意只有一个3。

在了解基本概念后,我们先说说如何做到求并集;聪明的同学马上就想到了并集等于数组合并加去重:

//ES6 求并集
function union(a, b) {
    return a.concat(b).filter((self, index, arr) => index === arr.indexOf(self));
};
console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]

当然使用存在兼容性的ES6会更简单:

//ES6 求并集
function union(a, b) {
    return Array.from(new Set([...a, ...b]));
};
console.log(union([1, 2, 3], [3, 4, 5])); //[1,2,3,4,5]

我们再来说说数组求交集,即元素同时存在两个数组中,因为太困了,这里我偷个懒使用了includes方法:

function intersect(a, b) {
    return a.filter(self => {
        return b.includes(self);
    });
};
console.log(intersect([1, 2, 3], [3, 4, 5]));//[3]

差集就好说了,在上方代码中includes前加个!即可,这里做个演示只求b数组的差集:

function difference (a, b) {
    return a.filter(self => {
        return !(b.includes(self));
    });
};
console.log(difference ([1, 2, 3], [3, 4, 5])); //[1, 2]

叁 总

那么到这里,我们借着汇总数组常见操作的契机,复习了数组常见API与部分容易忽略的知识。对于数组去重,降维,排序等操作都至少给出了一种解决思路。若有对于文中实现有更好的建议或疑问,也欢迎大家留言。我会在第一时间回复。另外,撕带油的游戏一定要小心小心再小心,不然就会像我这样毁掉一件衣服。

JS 数组常见操作汇总,数组去重、降维、排序、多数组合并实现思路整理 随笔 第2张

那么本文到这里就结束了,我是真的好困好困,我还没买到回家的票!!!!含泪睡觉。

扫码关注我们
微信号:SRE实战
拒绝背锅 运筹帷幄