學(xué)習(xí)Javascript之?dāng)?shù)組去重

前言

本文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

使用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方法耗時(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ù)書籍

前端進(jìn)階學(xué)習(xí)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末窗市,一起剝皮案震驚了整個(gè)濱河市先慷,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌咨察,老刑警劉巖论熙,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異摄狱,居然都是意外死亡脓诡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門媒役,熙熙樓的掌柜王于貴愁眉苦臉地迎上來祝谚,“玉大人,你說我怎么就攤上這事酣衷〗还撸” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長席爽。 經(jīng)常有香客問我意荤,道長,這世上最難降的妖魔是什么只锻? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任玖像,我火速辦了婚禮,結(jié)果婚禮上炬藤,老公的妹妹穿的比我還像新娘。我一直安慰自己碴里,他們只是感情好沈矿,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著咬腋,像睡著了一般羹膳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上根竿,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天陵像,我揣著相機(jī)與錄音,去河邊找鬼寇壳。 笑死醒颖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的壳炎。 我是一名探鬼主播泞歉,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼匿辩!你這毒婦竟也來了腰耙?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤铲球,失蹤者是張志新(化名)和其女友劉穎挺庞,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稼病,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡选侨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了然走。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片侵俗。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖丰刊,靈堂內(nèi)的尸體忽然破棺而出隘谣,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布寻歧,位于F島的核電站掌栅,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏码泛。R本人自食惡果不足惜猾封,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望噪珊。 院中可真熱鬧晌缘,春花似錦、人聲如沸痢站。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阵难。三九已至岳枷,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間呜叫,已是汗流浹背空繁。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留朱庆,地道東北人盛泡。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像娱颊,于是被迫代替她去往敵國和親饭于。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 方法一 For嵌套for 使用splice去重更改原數(shù)組 正向遍歷循環(huán) 遇到刪掉 原數(shù)組遞減1 { let a...
    金色888閱讀 357評(píng)論 0 0
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對(duì)象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,212評(píng)論 0 4
  • 關(guān)注 如何答一道驚艷面試官的數(shù)組去重問題? 為什么寫這篇文章颅痊? 數(shù)組去重應(yīng)該是面試必考問題之一殖熟。 雖然它是一道并不...
    冇得感情閱讀 224評(píng)論 0 1
  • JS數(shù)組去重是前端面試菱属,常考察的一道題舰罚,你會(huì)幾種方法呢纽门?如果你有更好的方法,可以在文章下放評(píng)論區(qū)提供营罢。接下來看看8...
    尤小小閱讀 18,508評(píng)論 9 13
  • JavaScript語言精粹 前言 約定:=> 表示參考相關(guān)文章或書籍; JS是JavaScript的縮寫赏陵。 本書...
    微笑的AK47閱讀 576評(píng)論 0 3