本文2895字,阅读大约须要12分钟。
总括: 本文总结了10种常见的数组去重方法,并将各类方法进行了对比。前端
如烟往事俱忘却,心底无私天地宽。面试
数组去重对于前端来讲不是一个常见的需求,通常后端都给作了,但这倒是一个有意思的问题,并且常常出如今面试中来考察面试者对JS的掌握程度。本文从数据类型的角度去思考数组去重这个问题,首先解决的是数组中只有基础数据类型的状况,而后是对象的去重。首先是咱们的测试数据:后端
var meta = [ 0, '0', true, false, 'true', 'false', null, undefined, Infinity, {}, [], function(){}, { a: 1, b: 2 }, { b: 2, a: 1 }, ]; var meta2 = [ NaN, NaN, Infinity, {}, [], function(){}, { a: 1, b: 2 }, { b: 2, a: 1 }, ]; var sourceArr = [...meta, ... Array(1000000) .fill({}) .map(() => meta[Math.floor(Math.random() * meta.length)]), ...meta2];
下文中引用的全部sourceArr
都是上面的变量。sourceArr
中包含了1000008
条数据。须要注意的是NaN
,它是JS中惟一一个和自身严格不相等的值。数组
而后咱们的目标是将上面的sourceArr
数组去重获得:dom
// 长度为14的数组 [false, "true", Infinity, true, 0, [], {}, "false", "0", null, undefined, {a: 1, b: 2}, NaN, function(){}]
这是在ES6中很经常使用的一种方法,对于简单的基础数据类型去重,彻底能够直接使用这种方法,扩展运算符 + Set
:函数
console.time('ES6中Set耗时:'); var res = [...new Set(sourceArr)]; console.timeEnd('ES6中Set耗时:'); // ES6中Set耗时:: 28.736328125ms console.log(res); // 打印数组长度20: [false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
或是使用Array.from + Set
:性能
console.time('ES6中Set耗时:'); var res = Array.from(new Set(sourceArr)); console.timeEnd('ES6中Set耗时:'); // ES6中Set耗时:: 28.538818359375ms console.log(res); // 打印数组长度20:[false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
优势:简洁方便,能够区分NaN
;学习
缺点:没法识别相同对象和数组;测试
简单的场景建议使用该方法进行去重。spa
使用内置的indexOf方法进行查找:
function unique(arr) { if (!Array.isArray(arr)) return; var result = []; for (var i = 0; i < arr.length; i++) { if (array.indexOf(arr[i]) === -1) { result.push(arr[i]) } } return result; } console.time('indexOf方法耗时:'); var res = unique(sourceArr); console.timeEnd('indexOf方法耗时:'); // indexOf方法耗时:: 23.376953125ms console.log(res); // 打印数组长度21: [false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN,NaN, function(){}, function(){}]
优势:ES5如下经常使用方法,兼容性高,易于理解;
缺点:没法区分NaN
;须要特殊处理;
能够在ES6如下环境使用。
和indexOf
相似,但inculdes
是ES7(ES2016)新增API:
function unique(arr) { if (!Array.isArray(arr)) return; var result = []; for (var i = 0; i < arr.length; i++) { if (!result.includes(arr[i])) { result.push(arr[i]) } } return result; } console.time('includes方法耗时:'); var res = unique(sourceArr); console.timeEnd('includes方法耗时:'); // includes方法耗时:: 32.412841796875ms console.log(res); // 打印数组长度20:[false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
优势:能够区分NaN
;
缺点:ES版本要求高,和indexOf
方法相比耗时较长;
这种方法比较巧妙,经过判断当前的index值和查找到的index是否相等来决定是否过滤元素:
function unique(arr) { if (!Array.isArray(arr)) return; return arr.filter(function(item, index, arr) { //当前元素,在原始数组中的第一个索引==当前索引值,不然返回当前元素 return arr.indexOf(item, 0) === index; }); } console.time('filter和indexOf方法耗时:'); var res = unique(sourceArr); console.timeEnd('filter和indexOf方法耗时:'); // includes方法耗时:: 24.135009765625ms console.log(res); // 打印数组长度19:[false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, function(){}, function(){}]
优势:利用高阶函数代码大大缩短;
缺点:因为indexOf
没法查找到NaN
,所以NaN
被忽略。
这种方法很优雅,代码量也不多,但和使用Set结构去重相比仍是美中不足。
一样是两个高阶函数的巧妙使用:
var unique = (arr) => { if (!Array.isArray(arr)) return; return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]); } var res = unique(sourceArr); console.time('reduce和includes方法耗时:'); var res = unique(sourceArr); console.timeEnd('reduce和includes方法耗时:'); // reduce和includes方法耗时:: 100.47802734375ms console.log(res); // 打印数组长度20:[false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
优势:利用高阶函数代码大大缩短;
缺点:ES版本要求高,速度较慢;
一样很优雅,但若是这种方法能用,一样也能用Set结构去重。
使用map实现:
function unique(arr) { if (!Array.isArray(arr)) return; let map = new Map(); let result = []; for (let i = 0; i < arr.length; i++) { if(map .has(arr[i])) { map.set(arr[i], true); } else { map.set(arr[i], false); result.push(arr[i]); } } return result; } console.time('Map结构耗时:'); var res = unique(sourceArr); console.timeEnd('Map结构耗时:'); // Map结构耗时:: 41.483154296875ms console.log(res); // 打印数组长度20:[false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
相比Set结构去重消耗时间较长,不推荐使用。
这个也比较经常使用,对数组进行双层遍历,挑出重复元素:
function unique(arr){ if (!Array.isArray(arr)) return; for(var i = 0; i < arr.length; i++) { for(var j = i + 1; j< arr.length; j++) { if(Object.is(arr[i], arr[j])) {// 第一个等同于第二个,splice方法删除第二个 arr.splice(j,1); j--; } } } return arr; } console.time('双层嵌套方法耗时:'); var res = unique(sourceArr); console.timeEnd('双层嵌套方法耗时:'); // 双层嵌套方法耗时:: 41500.452880859375ms console.log(res); // 打印数组长度20: [false, "true", Infinity, true, 0, [], [], {b: 2, a: 1}, {b: 2, a: 1}, {}, {}, "false", "0", null, undefined, {a: 1, b: 2}, {a: 1, b: 2}, NaN, function(){}, function(){}]
优势:兼容性高。
缺点:性能低,时间复杂度高。
不推荐使用。
这个思路也很简单,就是利用sort
方法先对数组进行排序,而后再遍历数组,将和相邻元素不相同的元素挑出来:
function unique(arr) { if (!Array.isArray(arr)) return; arr = arr.sort((a, b) => a - b); var result = [arr[0]]; for (var i = 1; i < arr.length; i++) { if (arr[i] !== arr[i-1]) { result.push(arr[i]); } } return result; } console.time('sort方法耗时:'); var res = unique(sourceArr); console.timeEnd('sort方法耗时:'); // sort方法耗时:: 936.071044921875ms console.log(res); // 数组长度357770,剩余部分省略 // 打印:(357770) [Array(0), Array(0), 0...]
优势:无;
缺点:耗时长,排序后数据不可控;
不推荐使用,由于使用sort方法排序没法对数字类型0
和字符串类型'0'
进行排序致使大量的冗余数据存在。
上面的方法只是针对基础数据类型,对于对象数组函数不考虑,下面再看下如何去重相同的对象。
下面的这种实现和利用Map结构类似,这里使用对象的key不重复的特性来实现
使用filter
和hasOwnProperty
方法:
function unique(arr) { if (!Array.isArray(arr)) return; var obj = {}; return arr.filter(function(item, index, arr) { return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true) }) } console.time('hasOwnProperty方法耗时:'); var res = unique(sourceArr); console.timeEnd('hasOwnProperty方法耗时:'); // hasOwnProperty方法耗时:: 258.528076171875ms console.log(res); // 打印数组长度13: [false, "true", Infinity, true, 0, [], {}, "false", "0", null, undefined, NaN, function(){}]
优势:代码简洁,能够区分相同对象数组函数;
缺点:版本要求高,由于要查找整个原型链所以性能较低;
该方法利用对象key不重复的特性来实现区分对象和数组,但上面是经过类型+值
作key的方式,因此{a: 1, b: 2}
和{}
被当作了相同的数据。所以该方法也有不足。
这种方法和使用Map
结构相似,但key
的组成有所不一样:
function unique(arr) { if (!Array.isArray(arr)) return; var result = []; var obj = {}; for (var i = 0; i < arr.length; i++) { var key = typeof arr[i] + JSON.stringify(arr[i]) + arr[i]; if (!obj[key]) { result.push(arr[i]); obj[key] = 1; } else { obj[key]++; } } return result; } console.time('对象方法耗时:'); var res = unique(sourceArr); console.timeEnd('对象方法耗时:'); // 对象方法耗时:: 585.744873046875ms console.log(res); // 打印数组长度15: [false, "true", Infinity, true, 0, [], {b: 2, a: 1}, {}, "false", "0", null, undefined, {a: 1, b: 2}, NaN, function(){}]
这种方法是比较成熟的,去除了重复数组和重复对象,但对于像{a: 1, b: 2}
和{b: 2, a: 1}
这种就没法区分,缘由在于将这两个对象进行JSON.stringify()
以后获得的字符串分别是{"a":1,"b":2}
和{"b":2,"a":1}
, 所以两个值算出的key不一样。加一个判断对象是否相等的方法就行了,改写以下:
function isObject(obj) { return Object.prototype.toString.call(obj) === '[object Object]'; } function unique(arr) { if (!Array.isArray(arr)) return; var result = []; var obj = {}; for (var i = 0; i < arr.length; i++) { // 此处加入对象和数组的判断 if (Array.isArray(arr[i])) { arr[i] = arr[i].sort((a, b) => a - b); } if (isObject(arr[i])) { let newObj = {} Object.keys(arr[i]).sort().map(key => { newObj[key]= arr[i][key]; }); arr[i] = newObj; } var key = typeof arr[i] + JSON.stringify(arr[i]) + arr[i]; if (!obj[key]) { result.push(arr[i]); obj[key] = 1; } else { obj[key]++; } } return result; } console.time('对象方法耗时:'); var res = unique(sourceArr); console.timeEnd('对象方法耗时:'); // 对象方法耗时:: 793.142822265625ms console.log(res); // 打印数组长度14: [false, "true", Infinity, true, 0, [], {b: 2, a: 1}, {}, "false", "0", null, undefined, NaN, function(){}]
方法 | 优势 | 缺点 |
---|---|---|
ES6中Set | 简单优雅,速度快 | 基础类型推荐使用。版本要求高,不支持对象数组和NaN |
使用indexOf | ES5如下经常使用方法,兼容性高,易于理解 | 没法区分NaN ;须要特殊处理 |
使用inculdes方法 | 能够区分NaN |
ES版本要求高,和indexOf 方法相比耗时较长 |
使用filter和indexOf方法 | 利用高阶函数代码大大缩短; | 因为indexOf 没法查找到NaN ,所以NaN 被忽略。 |
利用reduce+includes | 利用高阶函数代码大大缩短; | ES7以上才能使用,速度较慢; |
利用Map结构 | 无明显优势 | ES6以上, |
双层嵌套,使用splice删除重复元素 | 兼容性高 | 性能低,时间复杂度高,若是不使用Object.is 来判断则须要对NaN 特殊处理,速度极慢。 |
利用sort方法 | 无 | 耗时长,排序后数据不可控; |
利用hasOwnProperty和filter | :代码简洁,能够区分相同对象数组函数 | 版本要求高,由于要查找整个原型链所以性能较低; |
利用对象key不重复的特性 | 优雅,数据范围广 | Object推荐使用。代码比较复杂。 |
能力有限,水平通常,欢迎勘误,不胜感激。
订阅更多文章可关注公众号「前端进阶学习」,回复「666」,获取一揽子前端技术书籍