学习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」,获取一揽子前端技术书籍

前端进阶学习
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,937评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,503评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,712评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,668评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,677评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,601评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,975评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,637评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,881评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,621评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,710评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,387评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,971评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,947评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,189评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,805评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,449评论 2 342

推荐阅读更多精彩内容

  • 方法一 For嵌套for 使用splice去重更改原数组 正向遍历循环 遇到删掉 原数组递减1 { let a...
    金色888阅读 355评论 0 0
  • 第5章 引用类型(返回首页) 本章内容 使用对象 创建并操作数组 理解基本的JavaScript类型 使用基本类型...
    大学一百阅读 3,204评论 0 4
  • 关注 如何答一道惊艳面试官的数组去重问题? 为什么写这篇文章? 数组去重应该是面试必考问题之一。 虽然它是一道并不...
    冇得感情阅读 224评论 0 1
  • JS数组去重是前端面试,常考察的一道题,你会几种方法呢?如果你有更好的方法,可以在文章下放评论区提供。接下来看看8...
    尤小小阅读 18,508评论 9 13
  • JavaScript语言精粹 前言 约定:=> 表示参考相关文章或书籍; JS是JavaScript的缩写。 本书...
    微笑的AK47阅读 575评论 0 3