从JS数组中移除重复值

2348

我有一个非常简单的JavaScript数组,可能包含重复项。

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

我需要删除重复项并将唯一值放入一个新数组。

我可以指出我尝试过的所有代码,但我认为这没有用,因为它们不起作用。我接受jQuery解决方案。

类似问题:


95
_.uniq(peoplenames) 解决了这个问题。请参考 http://lodash.com/docs#uniq 了解更多信息。 - Connor Leech
10
@ConnorLeech 使用 lodash 很容易,但这不是最优化的方式。 - Suhail Mumtaz Awan
45
我认为最简单的方法是使用Set对象,它可以存储任何类型的唯一值。换句话说,Set会自动帮我们删除重复的元素。`const names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];let unique = [...new Set(names)]; console.log(unique); // 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl'` - Asif vora
13
世界上有太多叫Mike的人了——为什么不把他们移除掉呢?Nancy在这件事上被打败了。 - toad
3
在我的解决方案中,我会在过滤之前对数据进行排序:const result = data.sort().filter((v, idx, t) => idx==0 || v != t[idx-1]); - Didier68
显示剩余10条评论
54个回答

5918

简述

通过使用Set构造函数和展开语法

uniq = [...new Set(array)];

(请注意,变量uniq将是一个数组......new Set()将其转换为Set,但[...]将其转换回数组)


"聪明"但天真的方法

uniqueArray = a.filter(function(item, pos) {
    return a.indexOf(item) == pos;
})

基本上,我们遍历数组,并对每个元素检查其在数组中的第一个位置是否等于当前位置。显然,对于重复元素,这两个位置是不同的。

使用过滤器回调的第三个(“this array”)参数,可以避免对数组变量进行闭包:

uniqueArray = a.filter(function(item, pos, self) {
    return self.indexOf(item) == pos;
})

虽然这个算法简洁,但对于大型数组来说效率并不高(二次时间复杂度)。

哈希表来解救

function uniq(a) {
    var seen = {};
    return a.filter(function(item) {
        return seen.hasOwnProperty(item) ? false : (seen[item] = true);
    });
}

通常做法是将每个元素放入散列表中,然后立即检查其是否存在。这样可以使时间复杂度线性,但至少有两个缺点:

  • 由于在JavaScript中哈希键只能是字符串或符号,因此该代码没有区分数字和“数值字符串”。也就是说,uniq([1,“1”])将仅返回[1]
  • 出于同样的原因,所有对象将被视为相等:uniq([{foo:1},{foo:2}])将仅返回[{foo:1}].

也就是说,如果您的数组仅包含基元,并且您不关心类型(例如,它总是数字),则此解决方案是最佳的。

两全其美

一种通用解决方案结合了两种方法:对于基元,它使用哈希查找;对于对象,则使用线性搜索。

function uniq(a) {
    var prims = {"boolean":{}, "number":{}, "string":{}}, objs = [];

    return a.filter(function(item) {
        var type = typeof item;
        if(type in prims)
            return prims[type].hasOwnProperty(item) ? false : (prims[type][item] = true);
        else
            return objs.indexOf(item) >= 0 ? false : objs.push(item);
    });
}

sort | uniq

另一个选项是先对数组进行排序,然后移除每个与前面一个相等的元素:

function uniq(a) {
    return a.sort().filter(function(item, pos, ary) {
        return !pos || item != ary[pos - 1];
    });
}

注意,这种方法无法用于对象(因为所有对象对于sort来说都是相等的)。此外,我们会在原始数组上产生副作用,这不好!但是,如果您的输入已经排序,那么这就是一种可行的方法(只需从上面删除sort)。

通过“唯一”...

有时候希望根据某些标准唯一化列表,而不仅仅是相等,例如过滤出不同但共享某些属性的对象。这可以通过传递回调来优雅地完成。每个元素都会应用这个“键”回调函数,并且具有相等“键”的元素将被删除。由于key期望返回一个原始数据类型,因此哈希表在这里很好用:

function uniqBy(a, key) {
    var seen = {};
    return a.filter(function(item) {
        var k = key(item);
        return seen.hasOwnProperty(k) ? false : (seen[k] = true);
    })
}

一个特别有用的 key()JSON.stringify,它可以移除物理上不同但“看起来”相同的对象:

a = [[1,2,3], [4,5,6], [1,2,3]]
b = uniqBy(a, JSON.stringify)
console.log(b) // [[1,2,3], [4,5,6]]

如果key不是基本数据类型,你就必须使用线性搜索:

function uniqBy(a, key) {
    var index = [];
    return a.filter(function (item) {
        var k = key(item);
        return index.indexOf(k) >= 0 ? false : index.push(k);
    });
}

在ES6中,您可以使用Set

function uniqBy(a, key) {
    let seen = new Set();
    return a.filter(item => {
        let k = key(item);
        return seen.has(k) ? false : seen.add(k);
    });
}

或者一个Map

function uniqBy(a, key) {
    return [
        ...new Map(
            a.map(x => [key(x), x])
        ).values()
    ]
}

这两种方法都可以使用非原始键进行操作。

首个还是最后一个?

当通过键来移除对象时,可能需要保留“相等”对象的第一个或最后一个。

使用上面提到的 Set 可以保留第一个,而使用 Map 则可以保留最后一个:

function uniqByKeepFirst(a, key) {
    let seen = new Set();
    return a.filter(item => {
        let k = key(item);
        return seen.has(k) ? false : seen.add(k);
    });
}


function uniqByKeepLast(a, key) {
    return [
        ...new Map(
            a.map(x => [key(x), x])
        ).values()
    ]
}

//

data = [
    {a:1, u:1},
    {a:2, u:2},
    {a:3, u:3},
    {a:4, u:1},
    {a:5, u:2},
    {a:6, u:3},
];

console.log(uniqByKeepFirst(data, it => it.u))
console.log(uniqByKeepLast(data, it => it.u))

无论是Underscore还是Lo-Dash都提供了uniq方法,它们的算法基本上与上面的第一个代码片段相似,归结为以下内容:

var result = [];
a.forEach(function(item) {
     if(result.indexOf(item) < 0) {
         result.push(item);
     }
});

这是二次的,但有很多其他好处,例如包装本机的indexOf,可以通过键(在他们的术语中为iteratee)进行唯一化,并对已排序的数组进行优化。

如果您正在使用jQuery并且无法忍受任何没有美元符号的东西,则可以这样使用:

  $.uniqArray = function(a) {
        return $.grep(a, function(item, pos) {
            return $.inArray(item, a) === pos;
        });
  }

这句话是第一个片段的变体。

性能

在JavaScript中,函数调用的代价比较高,因此上述解决方案虽然简洁,但不是特别高效。为了获得最佳性能,请使用循环替换filter并消除其他函数调用:

function uniq_fast(a) {
    var seen = {};
    var out = [];
    var len = a.length;
    var j = 0;
    for(var i = 0; i < len; i++) {
         var item = a[i];
         if(seen[item] !== 1) {
               seen[item] = 1;
               out[j++] = item;
         }
    }
    return out;
}

这段丑陋的代码与上面第三个片段执行相同的操作,但快了一个数量级(截至2017年仅快了两倍- JS核心团队做得很好!)

function uniq(a) {
    var seen = {};
    return a.filter(function(item) {
        return seen.hasOwnProperty(item) ? false : (seen[item] = true);
    });
}

function uniq_fast(a) {
    var seen = {};
    var out = [];
    var len = a.length;
    var j = 0;
    for(var i = 0; i < len; i++) {
         var item = a[i];
         if(seen[item] !== 1) {
               seen[item] = 1;
               out[j++] = item;
         }
    }
    return out;
}

/////

var r = [0,1,2,3,4,5,6,7,8,9],
    a = [],
    LEN = 1000,
    LOOPS = 1000;

while(LEN--)
    a = a.concat(r);

var d = new Date();
for(var i = 0; i < LOOPS; i++)
    uniq(a);
document.write('<br>uniq, ms/loop: ' + (new Date() - d)/LOOPS)

var d = new Date();
for(var i = 0; i < LOOPS; i++)
    uniq_fast(a);
document.write('<br>uniq_fast, ms/loop: ' + (new Date() - d)/LOOPS)

ES6

ES6提供了Set对象,它使事情变得更加容易:

function uniq(a) {
   return Array.from(new Set(a));
}


let uniq = a => [...new Set(a)];

注意,与Python不同,ES6集合按插入顺序迭代,因此该代码保留了原始数组的顺序。
但是,如果你需要一个具有唯一元素的数组,为什么不从一开始就使用集合呢?
生成器
基于相同的基础,可以构建一个“惰性”的基于生成器的 uniq 版本:
- 从参数中获取下一个值 - 如果已经看到了它,请跳过它 - 否则,生成并将其添加到已看到的值的集合中

function* uniqIter(a) {
    let seen = new Set();

    for (let x of a) {
        if (!seen.has(x)) {
            seen.add(x);
            yield x;
        }
    }
}

// example:

function* randomsBelow(limit) {
    while (1)
        yield Math.floor(Math.random() * limit);
}

// note that randomsBelow is endless

count = 20;
limit = 30;

for (let r of uniqIter(randomsBelow(limit))) {
    console.log(r);
    if (--count === 0)
        break
}

// exercise for the reader: what happens if we set `limit` less than `count` and why


16
filter和indexOf在ECMAScript 5中被引入,因此在旧的IE版本(<9)中将无法工作。如果你关心这些浏览器,你将需要使用具有类似功能的库(如jQuery、underscore.js等)。 - Roman Bataev
14
如果你希望你的页面在旧版浏览器中正常工作,你可能需要这样做。 - Michael Robinson
13
这是一个 O(n^2) 的解法,在大数组中可能会运行非常缓慢... - seriyPS
9
尝试使用以下数组:["toString", "valueOf", "failed"]。完全删除 toStringvalueOf。使用 Object.create(null) 替代 {} - Charles Beattie
9
相比其他解决方案,有人知道 Set 转换解决方案有多快吗? - Eric Nguyen
显示剩余37条评论

517

使用jQuery快速而简单的方法:

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];
var uniqueNames = [];
$.each(names, function(i, el){
    if($.inArray(el, uniqueNames) === -1) uniqueNames.push(el);
});

375
如果有人不使用jQuery,我不介意提供一个非jQuery的解决方案。 - Matej
8
由于这个可靠的人已经将其改回原始的inArray解决方案,因此我要再次提醒:这个解决方案的时间复杂度为O(n^2),效率低下。 - Casey Kuball
46
我希望在2020年我们能够开始淘汰jQuery和其他更过时的解决方案... Stackoverflow网站正在显现一些老态... - Nick Steele
6
我同意@NickSteele的看法,但是如果你关注投票而不是被采纳的答案,你会发现这种情况自然而然地随着时间的推移发生。随着旧的已废弃答案被踩,最佳答案会向顶部聚集。 - Chris
1
JQuery 的初衷是提供文档遍历(现在已内置于浏览器中)、动画效果(现在有更快、更干净的替代方案)、事件处理(现在有更好的实现方式,也适用于 Node),以及 Ajax(Ajax 已在近十年前被 WebSocket 取代)。由于 JQuery 的四个方面都相对过时了,唯一使用 JQuery 的理由是如果你已经熟悉它,而且没有时间学习更好的东西。今天 JQuery 所做的一切都可以由其他库更好地完成,或者已经被完全取代。 - Nick Steele
显示剩余6条评论

388

看够了使用for循环或jQuery的所有不良示例。现在,Javascript拥有完美的工具:sort、map和reduce。

在保留现有顺序的情况下缩减唯一项

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

var uniq = names.reduce(function(a,b){
    if (a.indexOf(b) < 0 ) a.push(b);
    return a;
  },[]);

console.log(uniq, names) // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]

// one liner
return names.reduce(function(a,b){if(a.indexOf(b)<0)a.push(b);return a;},[]);

使用排序加速uniq

可能有更快的方法,但这个方法相当不错。

var uniq = names.slice() // slice makes copy of array before sorting it
  .sort(function(a,b){
    return a > b;
  })
  .reduce(function(a,b){
    if (a.slice(-1)[0] !== b) a.push(b); // slice(-1)[0] means last item in array without removing it (like .pop())
    return a;
  },[]); // this empty array becomes the starting value for a

// one liner
return names.slice().sort(function(a,b){return a > b}).reduce(function(a,b){if (a.slice(-1)[0] !== b) a.push(b);return a;},[]);

更新2015年:ES6版本:

在ES6中,您可以使用Set和Spread轻松高效地删除所有重复项:

var uniq = [ ...new Set(names) ]; // [ 'Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Carl' ]

按出现次数排序:

有人询问如何根据唯一名称的数量对结果进行排序:

var names = ['Mike', 'Matt', 'Nancy', 'Adam', 'Jenny', 'Nancy', 'Carl']

var uniq = names
  .map((name) => {
    return {count: 1, name: name}
  })
  .reduce((a, b) => {
    a[b.name] = (a[b.name] || 0) + b.count
    return a
  }, {})

var sorted = Object.keys(uniq).sort((a, b) => uniq[a] < uniq[b])

console.log(sorted)

不错!是否有可能根据重复对象的频率对数组进行排序?以便在上面的示例中,"Nancy"被移动到修改后的数组的前面(或后面)? - ALx
@ALx - 我更新了一个基于出现次数排序的示例。 - Christian Landgren
如果数据只是一个名称数组,除了消除重复项之外没有其他要求,为什么要使用排序、映射和归约呢?只需使用集合 - 在O(n)时间内完成任务。 -- https://msdn.microsoft.com/en-us/library/dn251547 - Dave
到目前为止,我看到的所有示例都与数组中的一个元素有关。如果您需要在多个元素上实现唯一性,该怎么办? - Jonny
为了更清晰,我个人会使用 const uniqueList = [ ...(new Set(names)) ];(带有额外的括号)。 - Alexander Mills
显示剩余7条评论

163

使用对象像 Set 一样删除重复项的原生 JS 方法

你可以尝试将数组放入一个对象中,然后遍历它的键来实现去除重复项:

function remove_duplicates(arr) {
    var obj = {};
    var ret_arr = [];
    for (var i = 0; i < arr.length; i++) {
        obj[arr[i]] = true;
    }
    for (var key in obj) {
        ret_arr.push(key);
    }
    return ret_arr;
}

使用Vanilla JS:通过跟踪已经看到的值来删除重复项(保持原有顺序)

或者,为了保持顺序,可以使用一个对象来存储所有先前看到的值,并在添加到数组之前检查这些值是否存在。

function remove_duplicates_safe(arr) {
    var seen = {};
    var ret_arr = [];
    for (var i = 0; i < arr.length; i++) {
        if (!(arr[i] in seen)) {
            ret_arr.push(arr[i]);
            seen[arr[i]] = true;
        }
    }
    return ret_arr;

}

ECMAScript 6:使用新的Set数据结构(有序)

ECMAScript 6增加了新的Set数据结构,使您能够存储任何类型的值。 Set.values以插入顺序返回元素。

function remove_duplicates_es6(arr) {
    let s = new Set(arr);
    let it = s.values();
    return Array.from(it);
}

使用示例:

a = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

b = remove_duplicates(a);
// b:
// ["Adam", "Carl", "Jenny", "Matt", "Mike", "Nancy"]

c = remove_duplicates_safe(a);
// c:
// ["Mike", "Matt", "Nancy", "Adam", "Jenny", "Carl"]

d = remove_duplicates_es6(a);
// d:
// ["Mike", "Matt", "Nancy", "Adam", "Jenny", "Carl"]

7
在更新的浏览器中,你甚至可以使用var c = Object.keys(b)。需要注意的是,这种方法只适用于字符串,但没关系,因为这也是最初的问题所要求的。 - amenthes
1
需要注意的是,由于对象不会按顺序保留其属性,因此您可能会失去数组的顺序。 - Ruan Mendes
1
@JuanMendes 我已经创建了一个安全排序的版本,如果该值之前没有被看到过,它将简单地复制到新数组中。 - Casey Kuball
在这一行 obj[arr[i]] = true; 发生了什么? - kittu
1
@kittu,这是获取数组的第i个元素,并将其放入对象(用作集合)中。键是元素,值是true,这完全是任意的,因为我们只关心对象的键。 - Casey Kuball
显示剩余2条评论

155

使用数组 .filter.indexOf 函数的单行版本:

arr = arr.filter(function (value, index, array) { 
  return array.indexOf(value) === index;
});

5
能否解释一下它如何消除重复项? - neelmeg
@web_dev:没错!我已经纠正了之前破坏代码的编辑。希望现在更有意义了。谢谢你的提问! - HBP
23
如果这是一个大数组,那么很遗憾这个方法的性能不好--arr.indexOf的时间复杂度为O(n),这使得整个算法的时间复杂度变成了O(n^2)。 - Casey Kuball
1
这个解决方案实际上非常慢,正如@CaseyKuball所建议的那样 - 请参见https://stackoverflow.com/questions/67424599/fastest-array-dedup-in-javascript - loretoparisi

76

使用Underscore.js

这是一个库,其中包含许多操作数组的函数。

它是与jQuery的礼服和Backbone.js的吊带相匹配的领带。

_.uniq

_.uniq(array, [isSorted], [iterator]) 别名:unique
使用 === 测试对象是否相等,生成array的无重复版本。如果您事先知道array已排序,则传递true将运行更快的算法。如果您想基于转换计算唯一项,请传递一个iterator函数。

示例

var names = ["Mike","Matt","Nancy","Adam","Jenny","Nancy","Carl"];

alert(_.uniq(names, false));

注意:Lo-Dash(一个Underscore的竞争对手)也提供了一个类似的.uniq实现。


Note: Lo-Dash (an underscore competitor) also offers a comparable .uniq implementation.

2
不幸的是,下划线库不提供定义自定义相等函数的能力。它们允许的回调函数是“迭代器”函数,例如带有参数(item,value,array)。 - Rene Wooller
[...new Set(Array)] 已经足够了,伙计。 - norbekoff
1
@norbekoff - 当然,哈哈。 ~10年后! - Brandon Boone

73

一行:

let names = ['Mike','Matt','Nancy','Adam','Jenny','Nancy','Carl', 'Nancy'];
let dup = [...new Set(names)];
console.log(dup);

5
如果您正在使用ES6,则最佳答案是: - kchetan
这三个点是什么意思? - Vitalicus
1
@Vitalicus,这是ES6中的扩展运算符。在此处阅读更多信息:(https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Spread_syntax) - Debargha Roy

67

你可以在JavaScript中简单地完成它,利用filter方法的第二个索引参数进行操作:

你只需使用JavaScript中的filter方法,并利用其提供的第二个索引参数即可轻松实现此功能:

var a = [2,3,4,5,5,4];
a.filter(function(value, index){ return a.indexOf(value) == index });

或者简写为

a.filter((v,i) => a.indexOf(v) == i)

这只适用于包含基本类型的数组? - frozen
a.indexOf(v) == i 应该改为 a.indexOf(v) === a.lastIndexOf(v) - Hitmands
5
@Hitmands,你是从右边进行比较的,我是从左边进行比较的,仅此而已。 - Ashutosh Jha
不需要 a 变量也可以工作,因为数组是 filter 的第三个参数:[1/0, 2,1/0,2,3].filter((v,i,a) => a.indexOf(v) === i)(请注意,它也可以与 Infinity 很好地配合使用☺) - Xenos
如果在使用map、reduce等方法后,你也可以使用.filter((v,i, array) => array.indexOf(v) == i) - omeanwell

43

可以像这样使用Array.filter()

var actualArr = ['Apple', 'Apple', 'Banana', 'Mango', 'Strawberry', 'Banana'];

console.log('Actual Array: ' + actualArr);

var filteredArr = actualArr.filter(function(item, index) {
  if (actualArr.indexOf(item) == index)
    return item;
});

console.log('Filtered Array: ' + filteredArr);

这可以在 ES6 中缩短为

actualArr.filter((item,index,self) => self.indexOf(item)==index);

这里有一个关于Array.filter()的简洁解释。


你能详细说明一下你在这里做了什么吗? :-) - Edwin
2
当数组是一个数组的数组时,它无法正常工作。 - DCR

39

使用本地JavaScript函数最简洁的方法来从数组中删除重复项是使用以下类似的序列:

vals.sort().reduce(function(a, b){ if (b != a[0]) a.unshift(b); return a }, [])

在reduce函数中没有必要使用slice或indexOf,就像我在其他示例中看到的那样!但与filter函数一起使用是有意义的:

不需要在reduce函数中使用slice或indexOf,这是其他示例中出现过的错误方法。然而,将它们与filter函数结合使用则是合理的:

vals.filter(function(v, i, a){ return i == a.indexOf(v) })

另一种ES6(2015)的方法是在一些浏览器上已经可以使用:

Array.from(new Set(vals))

甚至可以使用扩展运算符

[...new Set(vals)]

干杯!


Set非常适合那些习惯于Python的人,非常直观。可惜它们没有那些伟大的(并集、交集、差集)方法。 - caiohamamura
我选择了使用 set 机制的简单一行代码。这是为了一个自定义的自动化任务,所以我并不担心在最新版本的 Chrome 中使用它(在 jsfiddle 中)。然而,我仍然想知道最短的 所有浏览器兼容 的去重数组的方法。 - Alexander Dixon
集合是新规范的一部分,您应该使用排序/归约组合来确保跨浏览器兼容性。@AlexanderDixon - ivoputzer
.reduce() 不是跨浏览器兼容的,因为我需要应用一个 poly-fill。不过还是感谢您的回复。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce - Alexander Dixon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接