前言
本文2895字,閱讀大約需要12分鐘一忱。
總括: 本文總結(jié)了10種常見的數(shù)組去重方法莲蜘,并將各種方法進(jìn)行了對(duì)比。
- 公眾號(hào):「前端進(jìn)階學(xué)習(xí)」帘营,回復(fù)「666」票渠,獲取一攬子前端技術(shù)書籍
如煙往事俱忘卻,心底無私天地寬芬迄。
正文
數(shù)組去重對(duì)于前端來說不是一個(gè)常見的需求问顷,一般后端都給做了,但這卻是一個(gè)有意思的問題禀梳,而且經(jīng)常出現(xiàn)在面試中來考察面試者對(duì)JS的掌握程度杜窄。本文從數(shù)據(jù)類型的角度去思考數(shù)組去重這個(gè)問題,首先解決的是數(shù)組中只有基礎(chǔ)數(shù)據(jù)類型的情況算途,然后是對(duì)象的去重塞耕。首先是我們的測(cè)試數(shù)據(jù):
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
條數(shù)據(jù)嘴瓤。需要注意的是NaN
扫外,它是JS中唯一一個(gè)和自身嚴(yán)格不相等的值莉钙。
然后我們的目標(biāo)是將上面的sourceArr
數(shù)組去重得到:
// 長度為14的數(shù)組
[false, "true", Infinity, true, 0, [], {}, "false", "0", null, undefined, {a: 1, b: 2}, NaN, function(){}]
基礎(chǔ)數(shù)據(jù)類型
1. ES6中Set
這是在ES6中很常用的一種方法,對(duì)于簡單的基礎(chǔ)數(shù)據(jù)類型去重筛谚,完全可以直接使用這種方法磁玉,擴(kuò)展運(yùn)算符 + Set
:
console.time('ES6中Set耗時(shí):');
var res = [...new Set(sourceArr)];
console.timeEnd('ES6中Set耗時(shí):');
// ES6中Set耗時(shí):: 28.736328125ms
console.log(res);
// 打印數(shù)組長度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耗時(shí):');
var res = Array.from(new Set(sourceArr));
console.timeEnd('ES6中Set耗時(shí):');
// ES6中Set耗時(shí):: 28.538818359375ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):簡潔方便,可以區(qū)分NaN
驾讲;
缺點(diǎn):無法識(shí)別相同對(duì)象和數(shù)組蚊伞;
簡單的場(chǎng)景建議使用該方法進(jìn)行去重。
2. 使用indexOf
使用內(nèi)置的indexOf方法進(jìn)行查找:
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方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('indexOf方法耗時(shí):');
// indexOf方法耗時(shí):: 23.376953125ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):ES5以下常用方法吮铭,兼容性高厚柳,易于理解;
缺點(diǎn):無法區(qū)分NaN
;需要特殊處理沐兵;
可以在ES6以下環(huán)境使用别垮。
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方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('includes方法耗時(shí):');
// includes方法耗時(shí):: 32.412841796875ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):可以區(qū)分NaN
扎谎;
缺點(diǎn):ES版本要求高碳想,和indexOf
方法相比耗時(shí)較長;
4. 使用filter和indexOf方法
這種方法比較巧妙毁靶,通過判斷當(dāng)前的index值和查找到的index是否相等來決定是否過濾元素:
function unique(arr) {
if (!Array.isArray(arr)) return;
return arr.filter(function(item, index, arr) {
//當(dāng)前元素胧奔,在原始數(shù)組中的第一個(gè)索引==當(dāng)前索引值,否則返回當(dāng)前元素
return arr.indexOf(item, 0) === index;
});
}
console.time('filter和indexOf方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('filter和indexOf方法耗時(shí):');
// includes方法耗時(shí):: 24.135009765625ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):利用高階函數(shù)代碼大大縮短预吆;
缺點(diǎn):由于indexOf
無法查找到NaN
龙填,因此NaN
被忽略。
這種方法很優(yōu)雅拐叉,代碼量也很少岩遗,但和使用Set結(jié)構(gòu)去重相比還是美中不足。
5. 利用reduce+includes
同樣是兩個(gè)高階函數(shù)的巧妙使用:
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方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('reduce和includes方法耗時(shí):');
// reduce和includes方法耗時(shí):: 100.47802734375ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):利用高階函數(shù)代碼大大縮短凤瘦;
缺點(diǎn):ES版本要求高宿礁,速度較慢;
同樣很優(yōu)雅蔬芥,但如果這種方法能用梆靖,同樣也能用Set結(jié)構(gòu)去重。
6. 利用Map結(jié)構(gòu)
使用map實(shí)現(xiàn):
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結(jié)構(gòu)耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('Map結(jié)構(gòu)耗時(shí):');
// Map結(jié)構(gòu)耗時(shí):: 41.483154296875ms
console.log(res);
// 打印數(shù)組長度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結(jié)構(gòu)去重消耗時(shí)間較長笔诵,不推薦使用返吻。
7. 雙層嵌套,使用splice刪除重復(fù)元素
這個(gè)也比較常用乎婿,對(duì)數(shù)組進(jìn)行雙層遍歷测僵,挑出重復(fù)元素:
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])) {// 第一個(gè)等同于第二個(gè),splice方法刪除第二個(gè)
arr.splice(j,1);
j--;
}
}
}
return arr;
}
console.time('雙層嵌套方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('雙層嵌套方法耗時(shí):');
// 雙層嵌套方法耗時(shí):: 41500.452880859375ms
console.log(res);
// 打印數(shù)組長度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(){}]
優(yōu)點(diǎn):兼容性高次酌。
缺點(diǎn):性能低恨课,時(shí)間復(fù)雜度高。
不推薦使用岳服。
8. 利用sort方法
這個(gè)思路也很簡單剂公,就是利用sort
方法先對(duì)數(shù)組進(jìn)行排序,然后再遍歷數(shù)組吊宋,將和相鄰元素不相同的元素挑出來:
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方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('sort方法耗時(shí):');
// sort方法耗時(shí):: 936.071044921875ms
console.log(res);
// 數(shù)組長度357770纲辽,剩余部分省略
// 打印:(357770) [Array(0), Array(0), 0...]
優(yōu)點(diǎn):無璃搜;
缺點(diǎn):耗時(shí)長拖吼,排序后數(shù)據(jù)不可控;
不推薦使用这吻,因?yàn)槭褂胹ort方法排序無法對(duì)數(shù)字類型0
和字符串類型'0'
進(jìn)行排序?qū)е麓罅康娜哂鄶?shù)據(jù)存在吊档。
上面的方法只是針對(duì)基礎(chǔ)數(shù)據(jù)類型,對(duì)于對(duì)象數(shù)組函數(shù)不考慮唾糯,下面再看下如何去重相同的對(duì)象怠硼。
Object
下面的這種實(shí)現(xiàn)和利用Map結(jié)構(gòu)相似,這里使用對(duì)象的key不重復(fù)的特性來實(shí)現(xiàn)
9. 利用hasOwnProperty和filter
使用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方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('hasOwnProperty方法耗時(shí):');
// hasOwnProperty方法耗時(shí):: 258.528076171875ms
console.log(res);
// 打印數(shù)組長度13: [false, "true", Infinity, true, 0, [], {}, "false", "0", null, undefined, NaN, function(){}]
優(yōu)點(diǎn):代碼簡潔移怯,可以區(qū)分相同對(duì)象數(shù)組函數(shù)香璃;
缺點(diǎn):版本要求高,因?yàn)橐檎艺麄€(gè)原型鏈因此性能較低舟误;
該方法利用對(duì)象key不重復(fù)的特性來實(shí)現(xiàn)區(qū)分對(duì)象和數(shù)組葡秒,但上面是通過類型+值
做key的方式,所以{a: 1, b: 2}
和{}
被當(dāng)做了相同的數(shù)據(jù)嵌溢。因此該方法也有不足眯牧。
10. 利用對(duì)象key不重復(fù)的特性
這種方法和使用Map
結(jié)構(gòu)類似,但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('對(duì)象方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('對(duì)象方法耗時(shí):');
// 對(duì)象方法耗時(shí):: 585.744873046875ms
console.log(res);
// 打印數(shù)組長度15: [false, "true", Infinity, true, 0, [], {b: 2, a: 1}, {}, "false", "0", null, undefined, {a: 1, b: 2}, NaN, function(){}]
這種方法是比較成熟的赖草,去除了重復(fù)數(shù)組和重復(fù)對(duì)象炸站,但對(duì)于像{a: 1, b: 2}
和{b: 2, a: 1}
這種就無法區(qū)分,原因在于將這兩個(gè)對(duì)象進(jìn)行JSON.stringify()
之后得到的字符串分別是{"a":1,"b":2}
和{"b":2,"a":1}
, 因此兩個(gè)值算出的key不同疚顷。加一個(gè)判斷對(duì)象是否相等的方法就好了旱易,改寫如下:
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++) {
// 此處加入對(duì)象和數(shù)組的判斷
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('對(duì)象方法耗時(shí):');
var res = unique(sourceArr);
console.timeEnd('對(duì)象方法耗時(shí):');
// 對(duì)象方法耗時(shí):: 793.142822265625ms
console.log(res);
// 打印數(shù)組長度14: [false, "true", Infinity, true, 0, [], {b: 2, a: 1}, {}, "false", "0", null, undefined, NaN, function(){}]
結(jié)論
方法 | 優(yōu)點(diǎn) | 缺點(diǎn) |
---|---|---|
ES6中Set | 簡單優(yōu)雅,速度快 |
基礎(chǔ)類型推薦使用腿堤。版本要求高阀坏,不支持對(duì)象數(shù)組和NaN
|
使用indexOf | ES5以下常用方法,兼容性高笆檀,易于理解 | 無法區(qū)分NaN ;需要特殊處理 |
使用inculdes方法 | 可以區(qū)分NaN
|
ES版本要求高忌堂,和indexOf 方法相比耗時(shí)較長 |
使用filter和indexOf方法 | 利用高階函數(shù)代碼大大縮短; | 由于indexOf 無法查找到NaN 酗洒,因此NaN 被忽略士修。 |
利用reduce+includes | 利用高階函數(shù)代碼大大縮短枷遂; | ES7以上才能使用,速度較慢棋嘲; |
利用Map結(jié)構(gòu) | 無明顯優(yōu)點(diǎn) | ES6以上酒唉, |
雙層嵌套,使用splice刪除重復(fù)元素 | 兼容性高 | 性能低沸移,時(shí)間復(fù)雜度高痪伦,如果不使用Object.is 來判斷則需要對(duì)NaN 特殊處理,速度極慢雹锣。 |
利用sort方法 | 無 | 耗時(shí)長网沾,排序后數(shù)據(jù)不可控; |
利用hasOwnProperty和filter | :代碼簡潔蕊爵,可以區(qū)分相同對(duì)象數(shù)組函數(shù) | 版本要求高辉哥,因?yàn)橐檎艺麄€(gè)原型鏈因此性能較低; |
利用對(duì)象key不重復(fù)的特性 | 優(yōu)雅攒射,數(shù)據(jù)范圍廣 | Object推薦使用证薇。代碼比較復(fù)雜。 |
能力有限匆篓,水平一般浑度,歡迎勘誤,不勝感激鸦概。
訂閱更多文章可關(guān)注公眾號(hào)「前端進(jìn)階學(xué)習(xí)」箩张,回復(fù)「666」,獲取一攬子前端技術(shù)書籍