【學習筆記】JavaScript - 數組去重

去重是開發中常常碰到的一個問題,但在實際開發中大多數是後臺接口去重,簡單高效,基本不會讓前端處理去重。固然這並非說前端去重就沒有必要了,依然須要會熟練使用,本文主要介紹幾種常見的數組去重的方法的思路前端

爲了測試這些方法的性能,如下提供一個簡單測試模版用來計算數組去重的耗時數組

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,因此就不會再添加到新數組裏,從而達到了去重的效果markdown

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", {}]
// 兩個 true 直接去掉了
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()` 後獲得類數組轉換成數組的方式不同
```js
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 返回,這種方法沒法去掉對象NaNui

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]])
複製代碼

性能挺糟糕spa

image.png

filter + indexOfprototype

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]])
複製代碼

看起來代碼比較簡潔,可是性能也不怎麼樣。。。3d

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

去重 {}, 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])
複製代碼
相關文章
相關標籤/搜索