去重是开发中经常碰到的一个问题,但在实际开发中大多数是后台接口去重,简单高效,基本不会让前端处理去重。当然这并不是说前端去重就没有必要了,依然需要会熟练使用,本文主要介绍几种常见的数组去重的方法的思路
为了测试这些方法的性能,以下提供一个简单测试模版用来计算数组去重的耗时
const arr1 = Array.from(new Array(100000), (x, index)=>{
return index;
});
const arr2 = Array.from(new Array(50000), (x, index)=>{
return index+index;
});
const start = new Date().getTime();
console.log('开始数组去重');
function unique(a, b) {
let arr = a.concat(b);
// 数组去重
}
console.log('去重后的长度', unique(arr1, arr2).length);
let end = new Date().getTime();
console.log('耗时', end - start);
利用对象的属性不会重复的特性
这里运用了一个简单的哈希结构,当数组中的数据出现过一次后,就在 obj 中将这个元素的值的位置标记为 1(或 true),后面若出现相同的属性值,因为该位置已经是 1,所以就不会再添加到新数组里,从而达到了去重的效果
function unique(arr) {
if (!Array.isArray(arr)) return;
let obj = {};
let newArr = [];
for(let i = 0; i < arr.length; i ++) {
if(!obj[arr[i]]) {
obj[arr[i]] = 1;
newArr.push(arr[i]);
}
}
return newArr;
}
// [1, "true", 15, false, undefined, null, NaN, 0, "a", {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]);
// [1, "true", 15, false, undefined, null, NaN, 0, "a", {a:{b:1}}, [1,2]]
unique([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
利用上面的测试模板,该方法处理 15W 的数据大概需要 18ms
ES6 的 set 与解构赋值去重
ES6 新增了 Set 这一数据结构,类似于数组,但 Set 的成员具有唯一性,不考虑兼容性,这种去重的方法代码最少,这种方法还无法去重对象
function unique(arr) {
if (!Array.isArray(arr)) return;
return [...new Set(arr)];
}
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]);
// [1, "true", true, 15, false, "false", undefined, "undefined", null, "null", NaN, "NaN", 0, "a", {a: {b: 1}}, {a: {b: 1}}, [1, 2], [1, 2]]
unique([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
// ES6 的 Set 与 Array.from()
// 与上面方法类似,性能也差不多,只是 `new Set()` 后得到类数组转换成数组的方式不一样
function unique(arr) {
if (!Array.isArray(arr)) return;
return Array.from(new Set(arr));
}
Array.sort()
sort() 将数组进行排序然后比较相邻元素是否相等,从而排除重复项(这种方法只做了一次排序和一次循环,所以效率会比下面的方法要高),不能很好去重 NaN 和 对象
function unique(arr) {
if (!Array.isArray(arr)) return;
arr = arr.sort();
let res = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] !== arr[i-1]) {
res.push(arr[i]);
}
}
return res;
}
function unique(arr) {
if (!Array.isArray(arr)) return;
return arr.concat().sort().filter(function(item, index, arr){
return !index || item !== arr[index - 1]
})
}
// [0, 1, 15, NaN, NaN, "NaN", {}, {}, "a", false, null, "true", true, undefined]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}]);
// [0, 1,[1,2], [1,2], 15, NaN, NaN, "NaN", {}, {}, "a", false, "false", false, null, "null", "true", true, "undefined", undefined]
unique([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
利用 indexOf
indexOf() 方法可返回某个指定元素在数组中首次出现的位置,该方法首先定义一个空数组 res,然后调用 indexOf 方法对原来的数组进行遍历判断,若元素不在 res 中则将其 push 进 res,最后将 res 返回,这种方法无法去掉对象和NaN
function unique(arr) {
if (!Array.isArray(arr)) return;
let res = [];
for(let i = 0; i < arr.length; i ++) {
if (res.indexOf(arr[i]) == -1) {
res.push(arr[i]);
}
}
return res;
}
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
// [1, "true", true, 15, false, "false", undefined, "undefined", null, "null", NaN, NaN, "NaN", 0, "a", {}, {}, [1,2], [1,2]]
unique([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
性能挺糟糕
filter + indexOf
function unique(arr) {
if (!Array.isArray(arr)) return;
return arr.filter(function(item, index, arr) {
//当前元素,在原始数组中的第一个索引==当前索引值,否则返回当前元素
return arr.indexOf(item, 0) === index;
});
}
// [1, "true", true, 15, false, undefined, null, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
// [1, "true", true, 15, false, "false", undefined, "undefined", null, "null", "NaN", 0, "a", {}, {}, [1,2], [1,2]]
unique10([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
看起来代码比较简洁,但性能也不怎么样...
双层循环 + include
function unique(arr) {
if (!Array.isArray(arr)) {
console.log('type error!');
return;
}
var array =[];
for(var i = 0; i < arr.length; i++) {
if( !array.includes( arr[i]) ) {
//includes 检测数组是否有某个值
array.push(arr[i]);
}
}
return array;
}
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
// [1, "true", true, 15, false, "false", undefined, "undefined", null, "null", NaN, "NaN", 0, "a", {}, {}, [1,2], [1,2]]
unique([1,1,'true','true',true,true,15,15,false,'false',false, undefined,'undefined', null,'null', NaN, NaN,'NaN', 0, 0, 'a', 'a',{a:{b:1}},{a:{b:1}}, -0,+0, [1,2], [1,2]])
双层循环 + push
双重 for(或while)循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是 O(n^2),若数组长度很大则将会非常耗费内存
function unique(arr) {
if (!Array.isArray(arr)) return;
let res = [arr[0]]
for (let i = 1; i < arr.length; i++) {
let flag = true;
for (let j = 0; j < res.length; j++) {
if (arr[i] === res[j]) {
flag = false;
break;
}
}
if (flag) {
res.push(arr[i]);
}
}
return res;
}
// [1, "true", true, 15, false, undefined, null, NaN, NaN, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
这种方法无法去重 对象 和 NaN。但同样是双层循环,这个方法会比下面的方法性能好不少
双层循环 + splice
双层循环,外层循环元素,内层循环时比较值,值相同时,则删去这个值,这种方法无法去重 对象 和 NaN
function unique(arr) {
if (!Array.isArray(arr)) return;
let len = arr.length;
for(let i = 0; i < len; i ++) {
for (let j = i + 1; j < len; j ++) {
if (arr[i] == arr[j]) {
arr.splice(j, 1);
len --;
j --;
}
}
}
return arr;
}
// [/a/, /a/, "1", ƒ, {}, {}, NaN, NaN, null, "null", "undefined"]
unique([/a/, /a/, "1", 1, String, 1,{}, {}, String, NaN, NaN, null,'null', 'undefined',undefined])
这种方法占用的内存较高,效率也是最低的
hasOwnProperty
利用 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)
})
}
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
ES6 的 Map 数据结构
创建一个空 Map 数据结构,遍历需要去重的数组,把数组的每一个元素作为 key 存到 Map 中。由于 Map 中不会出现相同的 key 值,所以最终得到的就是去重后的结果
function unique(arr) {
let map = new Map();
let res = new Array(); // 数组用于返回结果
for (let i = 0; i < arr.length; i++) {
if(map.has(arr[i])) { // 如果有该 key 值
map.set(arr[i], true);
} else {
map.set(arr[i], false); // 如果没有该 key 值
res.push(arr[i]);
}
}
return res;
}
//1, "true", true, 15, false, undefined, "undefined", null, "null", NaN, "NaN", 0, "a", {}, {}
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, 'undefined',null,null, 'null',NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
reduce + includes
function unique(arr){
return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);
}
// [1, "true", true, 15, false, undefined, null, NaN, "NaN", 0, "a", {}, {}]
unique([1,1,'true','true',true,true,15,15,false,false, undefined,undefined, null,null, NaN, NaN,'NaN', 0, 0, 'a', 'a',{},{}])
去重 {}、NaN、undefined、null
{}的比较真心不好做,有残缺性的比较可以这样写JSON.stringify({}) == '{}'- ES5
for-in+call+for方案
// 判断空对象
function isEmptyObj (obj) {
if (Object.prototype.toString.call(obj) === "[object Object]") {
for (let i in obj) {
// 存在属性或方法,则不是空对象
return false
}
return true;
} else {
return false;
}
}
function unique (arr) {
let temp = [];
let emptyObjMark = true; // 标识位
let NaNObjMark = true; // 标识位
// 传入值须存在且长度小于等于 1 时直接返回数组
if (arr && arr.length <= 1) {
return arr;
} else {
// 遍历当前数组
for (let i = 0, len = arr.length; i < len; i++) {
// 标识位的作用是用来判断是否存在 NaN 和 空对象,第一次找到保留到新数组中
// 然后标识位改为 false 是为了再次找到时不推入数组
if (isEmptyObject(arr[i])) {
emptyObjMark && temp.indexOf(arr[i]) == -1 ? temp.push(arr[i]) : '';
emptyObjMark = false;
} else if (arr[i] !== arr[i]) {
NaNObjMark && temp.indexOf(arr[i]) == -1 ? temp.push(arr[i]) : '';
NaNObjMark = false;
} else {
temp.indexOf(arr[i]) == -1 ? temp.push(arr[i]) : '';
}
}
}
return temp;
}
// [1, "true", true, 5, "F", false, undefined, null, NaN, {}, "{}", 0, "a"]
unique([1,1,'true',true,true,5,'F',false, undefined, null,null,undefined, NaN,{},{},'{}', 0, 1, 'a', 'a', NaN])