學習Javascript之數組去重

前言

本文2895字,閱讀大約需要12分鐘。

總括: 本文總結了10種常見的數組去重方法,並將各種方法進行了對比。

  • 公衆號:「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍

如煙往事俱忘卻,心底無私天地寬

正文

數組去重對於前端來說不是一個常見的需求,一般後端都給做了,但這卻是一個有意思的問題,而且經常出現在面試中來考察面試者對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數組去重得到:

// 長度爲14的數組
[false, "true", Infinity, true, 0, [], {}, "false", "0", null, undefined, {a: 1, b: 2}, NaN, function(){}]

基礎數據類型

1. ES6中Set

這是在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

**缺點:**無法識別相同對象和數組;

簡單的場景建議使用該方法進行去重。

2. 使用indexOf

使用內置的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以下環境使用。

3. 使用inculdes方法

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方法相比耗時較長;

4. 使用filter和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結構去重相比還是美中不足。

5. 利用reduce+includes

同樣是兩個高階函數的巧妙使用:

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結構去重。

6. 利用Map結構

使用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結構去重消耗時間較長,不推薦使用。

7. 雙層嵌套,使用splice刪除重複元素

這個也比較常用,對數組進行雙層遍歷,挑出重複元素:

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

優點:兼容性高。

缺點:性能低,時間複雜度高。

不推薦使用。

8. 利用sort方法

這個思路也很簡單,就是利用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'進行排序導致大量的冗餘數據存在。

上面的方法只是針對基礎數據類型,對於對象數組函數不考慮,下面再看下如何去重相同的對象。

Object

下面的這種實現和利用Map結構相似,這裏使用對象的key不重複的特性來實現

9. 利用hasOwnProperty和filter

使用filterhasOwnProperty方法:

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}{}被當做了相同的數據。因此該方法也有不足。

10. 利用對象key不重複的特性

這種方法和使用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」,獲取一攬子前端技術書籍

前端進階學習

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章