JS 数组常见操作汇总,数组去重、降维、排序、多数组合并实现思路整理
壹 ❀ 引
JavaScript开发中数组加工极为常见,其次在面试中被问及的概率也特别高,一直想整理一篇关于数组常见操作的文章,本文也算了却心愿了。
说在前面,文中的实现并非最佳,实现虽然有很多种,但我觉得大家至少应该掌握一种,这样在面试能解决大部分数组问题。在了解实现思路后,日常开发中结合实际场景优化实现,提升性能也是后期该考虑的。
本文主要围绕数组去重、数组排序、数组降维、数组合并、数组过滤、数组求差集,并集,交集,数组是否包含某项等知识点展开,附带部分知识拓展,在看实现代码前也建议大家先自行思考,那么本文开始。
贰 ❀ 常见数组操作
贰 ❀ 壹 数组去重
数组去重我分为两种情况,简单数组去重与对象数组去重。所谓简单数组即元素均为基本数据类型,如下:
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创建一个新数组,新数组由被调用的数组元素组成,且元素顺序与原数组保持一致。元素复制操作中分为基本类型与引用类型两种情况:
数据类型如字符串,数字和布尔(不是
String
,Number
和Boolean
对象):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与部分容易忽略的知识。对于数组去重,降维,排序等操作都至少给出了一种解决思路。若有对于文中实现有更好的建议或疑问,也欢迎大家留言。我会在第一时间回复。另外,撕带油的东西一定要小心小心再小心,不然就会像我这样毁掉一件衣服。
那么本文到这里就结束了,我是真的好困好困,我还没买到回家的票!!!!含泪睡觉。