获取JavaScript数组中的所有唯一值(删除重复项)

2781
我有一个数字数组,我需要确保其中的数字是唯一的。我在互联网上找到了下面的代码片段,它在数组中有零的情况下工作得很好。我在Stack Overflow上找到了一个几乎完全相似的this other script,但它没有出错。
所以为了帮助我学习,有人可以帮我确定原型脚本出了什么问题吗?
Array.prototype.getUnique = function() {
 var o = {}, a = [], i, e;
 for (i = 0; e = this[i]; i++) {o[e] = 1};
 for (e in o) {a.push (e)};
 return a;
}

6
那个较旧的问题是关于查找并返回重复项的(我也感到困惑!)。我的问题更多地涉及到为什么此函数在数组中有零时会失败。 - Mottie
对于未来的读者,当你开始发现你必须要以算法方式频繁地修改数据结构的内容(排序、去重等),或者在每次迭代中搜索其中的元素时,可以安全地假设你一开始使用的数据结构是错误的,并开始使用更适合当前任务的数据结构(例如,在这种情况下,使用哈希集合而不是数组)。 - nurettin
我很久以前从别处复制了这段代码...但它似乎非常简单:o表示对象a表示数组i表示索引,而e则表示嗯,某个东西:P - Mottie
使用 Ramda 中的 R.uniq(list) 可以解决这个问题。https://ramdajs.com/docs/#uniq - varad_s
@user6316468 请注意代码部分的重点。火箭表情符号与示例代码无关。抱歉造成困惑。 - Lukas Liesis
显示剩余9条评论
95个回答

4531
JavaScript 1.6/ ECMAScript 5中,你可以使用数组的本地filter方法来按以下方式获取一个包含唯一值的数组:

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

// usage example:
var a = ['a', 1, 'a', 2, '1'];
var unique = a.filter(onlyUnique);

console.log(unique); // ['a', 1, 2, '1']

本地方法filter将循环遍历数组,并仅保留通过给定回调函数onlyUnique的条目。

onlyUnique检查给定值是否是第一次出现。如果不是,则必须是重复项,将不会被复制。

此解决方案无需任何额外的库,如jQuery或prototype.js。

它也适用于具有混合值类型的数组。

对于不支持本地方法filterindexOf的旧浏览器(<ie9),您可以在MDN文档中找到解决方法filterindexOf

如果要保留值的最后一次出现,请将indexOf替换为lastIndexOf

使用ES6,可以缩短为:

// usage example:
var myArray = ['a', 1, 'a', 2, '1'];
var unique = myArray.filter((value, index, array) => array.indexOf(value) === index);

console.log(unique); // unique is ['a', 1, 2, '1']

感谢Camilo Martin在评论中的提示。

ES6有一个原生对象Set用于存储唯一值。现在你可以这样获取一个具有唯一值的数组:

var myArray = ['a', 1, 'a', 2, '1'];

let unique = [...new Set(myArray)];

console.log(unique); // unique is ['a', 1, 2, '1']

Set 的构造函数需要一个可迭代对象,例如一个数组,并且展开运算符 ... 可以将 Set 转换回数组。感谢 Lukas Liese 在评论中提供的提示。


112
很不幸,这个解决方案会运行得慢很多。你正在进行两次循环,一次使用 filter 函数,一次使用索引。 - Jack Franzen
49
在现代JavaScript中,.filter((v,i,a)=>a.indexOf(v)==i)表示筛选出数组中不重复的元素(使用箭头函数语法)。 - Camilo Martin
385
让唯一数值 = [...new Set(随机数组)]; - Lukas Liesis
28
为了避免任何新手被性能影响所吓倒,需要注意的是“明显变慢”可能只有纳秒级别。如果你的数组相对较小(百个或更少),使用像这样简洁的一次性版本还是很不错的,原因在于可读性和可维护性等方面,而不仅仅是性能。但是,set版本也非常好且简洁。 - Vectorjohn
22
在Chrome 100中比较了使用.filter+indexOf和Set方法处理数字数组的性能,结果显示,在长度为0到120的数组中,filter方法更快。但如果数组长度达到200,filter方法需要比Set方法多50%的时间(6微秒对比9微秒)。当元素数量达到5000时,filter操作需要超过3毫秒,而Set方法仍然可以在173微秒内完成示例。因此,“慢得多”这个结论实际上取决于具体的应用场景。如果有人感兴趣,我可以将数据作为单独的问答发布。 - Christian
显示剩余18条评论

1796

ES6/ES2015更新的答案: 使用Set展开运算符(感谢le-m),单行解决方案如下:

let uniqueItems = [...new Set(items)]

返回哪个值

[4, 5, 6, 3, 2, 23, 1]

24
请注意,内部数组不会起作用 Array.from(new Set([[1,2],[1,2],[1,2,3]])) - Alexander Goncharov
114
请注意,如果您使用Set并添加对象而不是原始值,则其中包含对对象的唯一引用。因此,在let s = new Set([{Foo:"Bar"}, {Foo:"Bar"}]);中,集合s将返回Set { { Foo: 'Bar' }, { Foo: 'Bar' } },这是一个包含相同值的对象的唯一对象引用的Set。如果你写let o = {Foo:"Bar"};,然后像这样创建两个引用的集合:let s2 = new Set([o,o]);,那么s2将是Set { { Foo: 'Bar' } } - mortb
6
如果有人想知道,这对字符串也适用,例如[...new Set(["apple","apple","orange"])]的结果是['apple','orange']。太好了! - Marquez
在Typescript中,使用Array.from( new Set( items ) ) - Lee Goddard
这个答案应该被标记为正确的。 - StefanGarofalo

345

我将所有答案分为4种可能的解决方案:

  1. 使用对象 { } 防止重复
  2. 使用辅助数组 [ ]
  3. 使用 filter + indexOf
  4. 额外福利!ES6 Sets 方法。

以下是在答案中找到的示例代码:

使用对象 { } 防止重复

function uniqueArray1( ar ) {
  var j = {};

  ar.forEach( function(v) {
    j[v+ '::' + typeof v] = v;
  });

  return Object.keys(j).map(function(v){
    return j[v];
  });
} 

使用辅助数组 [ ]

function uniqueArray2(arr) {
    var a = [];
    for (var i=0, l=arr.length; i<l; i++)
        if (a.indexOf(arr[i]) === -1 && arr[i] !== '')
            a.push(arr[i]);
    return a;
}

使用 filter + indexOf

function uniqueArray3(a) {
  function onlyUnique(value, index, self) { 
      return self.indexOf(value) === index;
  }

  // usage
  var unique = a.filter( onlyUnique ); // returns ['a', 1, 2, '1']

  return unique;
}

使用 ES6 [...new Set(a)]

function uniqueArray4(a) {
  return [...new Set(a)];
}

我曾经想知道哪个更快。我制作了一个Google表格来测试函数。注意:Google Sheets不支持ECMA 6,所以无法进行测试。

测试结果如下: enter image description here

我希望看到使用对象{}的代码会赢,因为它使用哈希。所以我很高兴这些测试在Chrome和IE中显示了这个算法的最佳结果。感谢@rab提供代码

2020年更新

Google脚本已启用ES6引擎。现在我测试了最后的代码,使用Sets比对象方法更快。


11
Makrov,所以 uniqueItems = [...new Set(items)] 这种方式似乎是所有方法中最快且最简洁的? - Vass
1
你的解决方案只处理原始类型,无法处理对象,你需要对哈希中的v使用JSON.stringify。 - Tofandel
3
你做了一个很棒的时间测试比较,非常感谢。我现在用了一个包含成千上万条目的数组进行了测试,确实 uniqueItems = [...new Set(items)] 是最快的方法。 - João Pimentel Ferreira
"Object.keys(j).map ..." 可以改为使用 Object.values(j); - Taysky

164
你也可以使用underscore.js


console.log(_.uniq([1, 2, 1, 3, 1, 4]));
<script src="http://underscorejs.org/underscore-min.js"></script>

这将返回:

[1, 2, 3, 4]

29
大家请这样做。不要将任何东西添加到Array原型上。拜托了。 - Jacob Dalton
92
@JacobDalton 请不要这样做。没有必要添加额外的库,只需使用 array = [...new Set(array)] 即可完成一个小任务。 - user6269864
@JacobDalton 为什么不呢?将某些东西“插入”到数组中有什么不好的地方吗? - anshul
如果您想使用对象相等语义查找唯一的 _objects_,Lodash 提供了 _.uniqWith(arrayOfObjects, _.isEqual)_.uniqWith([{a: 2}, {b: 3}, {a: 2}], _.isEqual) 将返回 [{a: 2}, {b: 3}] - Aron
这是一个整体上不错的解决方案,但如果你已经在依赖项中有一个库来完成这项工作,那么这肯定是最好的解决方案。这个答案激发了我查找我的依赖项中的某些内容,结果我们已经在使用 lodash,它已经有一个 uniq() 函数了。所以,即使它不是同一个库,感谢你提供的信息! - brandizzi

145

一句话,纯 JavaScript

使用 ES6 语法

list = list.filter((x, i, a) => a.indexOf(x) == i)

x --> item in array
i --> index of item
a --> array reference, (in this case "list")

图片描述文字

使用 ES5 语法

list = list.filter(function (x, i, a) { 
    return a.indexOf(x) == i; 
});

浏览器兼容性: IE9+


101
使用 Set 去除重复项。 带有重复项的 Array
const withDuplicates = [2, 2, 5, 5, 1, 1, 2, 2, 3, 3];

使用Set获取没有重复项的新数组

const withoutDuplicates = Array.from(new Set(withDuplicates));
一种更短的版本
const withoutDuplicates = [...new Set(withDuplicates)];

结果:[2, 5, 1, 3]


2
完全复制了几年前其他人给出的答案。 - vsync
也许可以使用一个新的Set来去除重复项。 - mdmundo

75

这里的许多答案可能对初学者不太有用。如果去重一个数组很困难,他们真的会知道原型链,甚至是jQuery吗?

在现代浏览器中,一个简单而干净的解决方案是将数据存储在Set中,它被设计为一个唯一值列表。

const cars = ['Volvo', 'Jeep', 'Volvo', 'Lincoln', 'Lincoln', 'Ford'];
const uniqueCars = Array.from(new Set(cars));
console.log(uniqueCars);

Array.from函数非常有用,可以将Set转换为Array,这样就可以轻松访问数组的所有方法和特性。还有其他方法可以实现相同的功能,具体可参考这里。但你可能根本不需要使用Array.from,因为Set已经拥有许多有用的特性,例如forEach

如果需要支持旧版Internet Explorer,无法使用Set,则可以采用简单的技巧,将项目复制到新数组中,并在此之前检查它们是否已经存在于新数组中。

// Create a list of cars, with duplicates.
var cars = ['Volvo', 'Jeep', 'Volvo', 'Lincoln', 'Lincoln', 'Ford'];
// Create a list of unique cars, to put a car in if we haven't already.
var uniqueCars = [];

// Go through each car, one at a time.
cars.forEach(function (car) {
    // The code within the following block runs only if the
    // current car does NOT exist in the uniqueCars list
    // - a.k.a. prevent duplicates
    if (uniqueCars.indexOf(car) === -1) {
        // Since we now know we haven't seen this car before,
        // copy it to the end of the uniqueCars list.
        uniqueCars.push(car);
    }
});

为了使这个代码片段可以被立即重复使用,让我们将它放在一个函数中。
function deduplicate(data) {
    if (data.length > 0) {
        var result = [];

        data.forEach(function (elem) {
            if (result.indexOf(elem) === -1) {
                result.push(elem);
            }
        });

        return result;
    }
}

所以为了去除重复项,我们现在会这样做。
var uniqueCars = deduplicate(cars);
deduplicate(cars)部分在函数完成后会成为我们命名为result的东西。 只需将任何数组的名称传递给它即可。

如果我想让新数组不是唯一的,而是一个包含重复值的数组,那该怎么做呢?以上面的例子为例,我要找的数组是["volvo","lincoln"] - Jason
@Jason 我会创建一个 Map 来存储之前出现过的项,以及一个数组来存储重复的项。然后循环遍历 cars 数组并检查 Map 是否有当前项,如果有,则将其推送到重复项数组中,如果没有,则将其添加到 Map 中。如果您创建一个新问题,我们可以在那里继续讨论,我很乐意为您创建一个代码示例。 - Seth Holladay
请注意,如果数组为空,则该函数不返回任何内容。 - Tranzium
很遗憾你没有展示set方法作为数组原型的扩展。 - johny why
我故意没有展示那个。我认为修改Array.prototype是一种不好的做法,并且强烈建议不要这样做。除了其他原因外,如果将来在语言中添加新的方法,这样做可能会导致未来出现错误。事实上,如果您的网站很受欢迎,甚至可能阻止将来向语言中添加新方法。这实际上已经发生过。请参见:https://2ality.com/2022/03/naming-conflicts.html - Seth Holladay

69

使用 ES6 的 new Set

var array = [3,7,5,3,2,5,2,7];
var unique_array = [...new Set(array)];
console.log(unique_array);    // output = [3,7,5,2]

使用 For Loop

var array = [3,7,5,3,2,5,2,7];

for(var i=0;i<array.length;i++) {
    for(var j=i+1;j<array.length;j++) {
        if(array[i]===array[j]) {
            array.splice(j,1);
        }
    }
}
console.log(array); // output = [3,7,5,2]


53

我后来发现了一个使用jQuery的不错方法

arr = $.grep(arr, function(v, k){
    return $.inArray(v ,arr) === k;
});

注意:此代码来自Paul Irish的鸭子补丁文章-我忘记给他点赞了:P


12
简洁的解决方案,但调用 inArray 比调用 hasOwnProperty 更不高效。 - Mister Smith
1
这也是O(N^2),对吧?而使用字典或hasOwnProperty方法可能会是O(N*logN)。 - speedplane

51

魔法

a.filter( e => !(t[e]=e in t) ) 

O(n) 性能 - 我们假设你的数组在 a 中,t={}。解释 这里 (+Jeppe impr.)

let unique = (a,t={}) => a.filter(e=>!(t[e]=e in t));

// "stand-alone" version working with global t:
// a1.filter((t={},e=>!(t[e]=e in t)));

// Test data
let a1 = [5,6,0,4,9,2,3,5,0,3,4,1,5,4,9];
let a2 = [[2, 17], [2, 17], [2, 17], [1, 12], [5, 9], [1, 12], [6, 2], [1, 12]];
let a3 = ['Mike', 'Adam','Matt', 'Nancy', 'Adam', 'Jenny', 'Nancy', 'Carl'];

// Results
console.log(JSON.stringify( unique(a1) ))
console.log(JSON.stringify( unique(a2) ))
console.log(JSON.stringify( unique(a3) ))


75
这看起来非常酷,如果没有充分的解释,我感觉你将会在我运行这个东西时挖掘比特币。 - Ondřej Želazko
5
我的意思是你应该加上一些解释和注释来扩展你的答案,使之更易懂,但不要改变原意。不要期望人们会像这样找到有用的答案(尽管看起来很酷,可能也有效)。 - Ondřej Želazko
2
不是魔法,但很像“Set”答案,在字典中使用O(1)键查找。你需要递增计数器吗?"e=>!(t[e]=e in t)"怎么样?虽然是个好答案。 - Jeppe
2
@Jeppe 当我运行你的改进时,我体验到了啊哈效应(之前我不知道除了for循环以外的其他结构中也可以使用in运算符:P)- 谢谢 - 我很感激,并会给你其他好答案+2。 - Kamil Kiełczewski
2
解决方案确实很好,但仅适用于元组。 以下示例不正确: unique(['2', 2]) // ['2']; unique([[1, 7], [1, '7'], ['1', 7], ['1', '7']]) // [1, 7] 因此,在使用时要小心。 - Max Starling

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