JavaScript开发中数组加工极为常见,其次在面试中被问及的几率也特别高,一直想整理一篇关于数组常见操做的文章,本文也算了却心愿了。javascript
说在前面,文中的实现并不是最佳,实现虽然有不少种,但我以为你们至少应该掌握一种,这样在面试能解决大部分数组问题。在了解实现思路后,平常开发中结合实际场景优化实现,提高性能也是后期该考虑的。html
本文主要围绕数组去重、数组排序、数组降维、数组合并、数组过滤、数组求差集,并集,交集,数组是否包含某项等知识点展开,附带部分知识拓展,在看实现代码前也建议你们先自行思考,那么本文开始。java
数组去重我分为两种状况,简单数组去重与对象数组去重。所谓简单数组即元素均为基本数据类型,以下:面试
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来讲,两个长得相同的对象只是外观相同,它们的引用地址并不一样,好比:app
[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与部分容易忽略的知识。对于数组去重,降维,排序等操做都至少给出了一种解决思路。如有对于文中实现有更好的建议或疑问,也欢迎你们留言。我会在第一时间回复。另外,撕带油的游戏必定要当心当心再当心,否则就会像我这样毁掉一件衣服。
那么本文到这里就结束了,我是真的好困好困,我还没买到回家的票!!!!含泪睡觉。