JavaScript - 数组去重

183 阅读2分钟

去重是开发中经常碰到的一个问题,但在实际开发中大多数是后台接口去重,简单高效,基本不会让前端处理去重。当然这并不是说前端去重就没有必要了,依然需要会熟练使用,本文主要介绍几种常见的数组去重的方法的思路

为了测试这些方法的性能,以下提供一个简单测试模版用来计算数组去重的耗时

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

image.png

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]])

image.png

// 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 中则将其 pushres,最后将 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]])

性能挺糟糕

image.png

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]])

看起来代码比较简洁,但性能也不怎么样...

image.png

双层循环 + 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]])

image.png

双层循环 + 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。但同样是双层循环,这个方法会比下面的方法性能好不少

image.png

双层循环 + 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]) 

这种方法占用的内存较高,效率也是最低的

image.png

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',{},{}])

image.png

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',{},{}])

image.png

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',{},{}])

image.png

去重 {}NaNundefinednull

  • {} 的比较真心不好做,有残缺性的比较可以这样写 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])