本文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中惟一一個和自身嚴格不相等的值。markdown
而後咱們的目標是將上面的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
;學習
**缺點:**沒法識別相同對象和數組;測試
簡單的場景建議使用該方法進行去重。
使用內置的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
相似,但includes
是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 ;須要特殊處理 |
使用includes方法 | 能夠區分NaN |
ES版本要求高,和indexOf 方法相比耗時較長 |
使用filter和indexOf方法 | 利用高階函數代碼大大縮短; | 因爲indexOf 沒法查找到NaN ,所以NaN 被忽略。 |
利用reduce+includes | 利用高階函數代碼大大縮短; | ES7以上才能使用,速度較慢; |
利用Map結構 | 無明顯優勢 | ES6以上, |
雙層嵌套,使用splice刪除重複元素 | 兼容性高 | 性能低,時間複雜度高,若是不使用Object.is 來判斷則須要對NaN 特殊處理,速度極慢。 |
利用sort方法 | 無 | 耗時長,排序後數據不可控; |
利用hasOwnProperty和filter | :代碼簡潔,能夠區分相同對象數組函數 | 版本要求高,由於要查找整個原型鏈所以性能較低; |
利用對象key不重複的特性 | 優雅,數據範圍廣 | Object推薦使用。代碼比較複雜。 |
能力有限,水平通常,歡迎勘誤,不勝感激。
訂閱更多文章可關注公衆號「前端進階學習」,回覆「666」,獲取一攬子前端技術書籍