如何从一个对象数组中删除所有重复项?

905
我有一个包含对象数组的对象。
obj = {};

obj.arr = new Array();

obj.arr.push({place:"here",name:"stuff"});
obj.arr.push({place:"there",name:"morestuff"});
obj.arr.push({place:"there",name:"morestuff"});

我想知道从数组中删除重复对象的最佳方法是什么。例如,obj.arr将变为...

{place:"here",name:"stuff"},
{place:"there",name:"morestuff"}

你的意思是如何阻止一个带有相同参数的散列表/对象被添加到数组中吗? - Matthew Lock
11
如果在数组中首次添加对象时防止重复会更简单,而不是之后再进行过滤,那么这也可以。 - Travis
3
即使是非常长的答案,但 MDN 可能有最短的答案:arrayWithNoDuplicates = Array.from(new Set(myArray)) - tonkatata
10
这在处理对象数组时无法正常运作。 - Debu Shinobi
你好,请查看下面一个简单且可重复使用的方法来管理重复项:https://stackoverflow.com/a/74544470/12930883 - RED-ONE
2
感谢@tonkatata的启发。可以使用Array.from(new Set(myArray.map(e => JSON.stringify(e)))))来创建对象数组。 - undefined
78个回答

948

加点 ES6 魔法怎么样?

obj.arr = obj.arr.filter((value, index, self) =>
  index === self.findIndex((t) => (
    t.place === value.place && t.name === value.name
  ))
)

参考网址

一个更通用的解决方案是:

const uniqueArray = obj.arr.filter((value, index) => {
  const _value = JSON.stringify(value);
  return index === obj.arr.findIndex(obj => {
    return JSON.stringify(obj) === _value;
  });
});

使用上述属性策略,而不是JSON.stringify

const isPropValuesEqual = (subject, target, propNames) =>
  propNames.every(propName => subject[propName] === target[propName]);

const getUniqueItemsByProperties = (items, propNames) => 
  items.filter((item, index, array) =>
    index === array.findIndex(foundItem => isPropValuesEqual(foundItem, item, propNames))
  );

如果你想让propNames属性可以是一个数组或者一个值,你可以添加一个包装器:

const getUniqueItemsByProperties = (items, propNames) => {
  const propNamesArray = Array.from(propNames);

  return items.filter((item, index, array) =>
    index === array.findIndex(foundItem => isPropValuesEqual(foundItem, item, propNamesArray))
  );
};

允许使用getUniqueItemsByProperties('a')getUniqueItemsByProperties(['a']);

Stackblitz示例

解释

  • 首先理解两种方法的用法:
  • 接下来,将您认为使两个对象相等的想法记在心中。
  • 如果满足我们刚刚想到的标准,但其位置不在具有该标准的对象的第一个实例,则可以将其检测为重复项。
  • 因此,我们可以使用上述标准来确定某些内容是否为重复项。

115
可以简化为:things.thing = things.thing.filter((thing, index, self) => self.findIndex(t => t.place === thing.place && t.name === thing.name) === index) - Josh Cole
10
@vsync 只需采纳 @BKM 的答案并将其组合起来,通用解决方案如下: const uniqueArray = arrayOfObjects.filter((object,index) => index === arrayOfObjects.findIndex(obj => JSON.stringify(obj) === JSON.stringify(object))); http://jsfiddle.net/x9ku0p7L/28/ - Eydrian
33
关键在于findIndex()方法返回的是第一个元素的索引,因此如果有第二个匹配的元素,则它将永远不会在过滤器中被找到并添加。我盯着它看了一分钟 :) - JBaczuk
4
一个问题,这不是O(n^2)的方法吗?假设我正在处理30条记录,那么我将执行900次迭代,对吗?(最坏的情况下,没有重复项) - Jose A
11
如果你有一个包含 200,000 个元素的数组,那么需要进行 400亿次迭代。在处理大型数组时,不应使用这种方法,应该始终使用 map(映射)来代替。请注意不要改变原始意思,并使翻译更加通俗易懂。 - JP_
显示剩余13条评论

506

使用过滤器的单行代码(保留顺序)

在数组中查找唯一的id

arr.filter((v,i,a)=>a.findIndex(v2=>(v2.id===v.id))===i)
如果顺序不重要,使用映射方案会更快:带有映射的解决方案
按多个属性(place和name)去重
arr.filter((v,i,a)=>a.findIndex(v2=>['place','name'].every(k=>v2[k] ===v[k]))===i)

所有属性都唯一(对于大型数组来说,这可能会很慢)

arr.filter((v,i,a)=>a.findIndex(v2=>(JSON.stringify(v2) === JSON.stringify(v)))===i)

通过将 findIndex 替换为 findLastIndex,保留最后一次出现。

arr.filter((v,i,a)=>a.findLastIndex(v2=>(v2.place === v.place))===i)

54
v, i, a 表示的含义是:value(值),index(索引),array(数组)。 - James B
8
如果键不是按相同顺序排列的话,这段代码会失效。它的功能是筛选数组中唯一的元素,并保留它们的顺序。 - Jamal Hussain
findIndex 中的 t 代表什么? - Bernardo Marques
1
简直美轮美奂 - avalanche1
5
如果能够解释这些代码的作用,以及使用易读的命名规范而不是试图预先压缩代码,那么这将会更好。 - Heretic Monkey
显示剩余6条评论

310

使用ES6+,您可以通过一行代码按键获取唯一对象列表:

const key = 'place';
const unique = [...new Map(arr.map(item => [item[key], item])).values()]

它可以放入一个函数中:

function getUniqueListBy(arr, key) {
    return [...new Map(arr.map(item => [item[key], item])).values()]
}

以下是一个可工作的示例:

const arr = [
    {place: "here",  name: "x", other: "other stuff1" },
    {place: "there", name: "x", other: "other stuff2" },
    {place: "here",  name: "y", other: "other stuff4" },
    {place: "here",  name: "z", other: "other stuff5" }
]

function getUniqueListBy(arr, key) {
    return [...new Map(arr.map(item => [item[key], item])).values()]
}

const arr1 = getUniqueListBy(arr, 'place')

console.log("Unique by place")
console.log(JSON.stringify(arr1))

console.log("\nUnique by name")
const arr2 = getUniqueListBy(arr, 'name')

console.log(JSON.stringify(arr2))

它是如何工作的

首先,数组被重新映射为可以用作 Map 的输入。

arr.map(item => [item[key], item]);

这意味着数组的每个元素都会被转换为另一个包含两个元素的数组;第一个元素是所选键,第二个元素是整个初始项,这称为 entry(例如array entriesmap entries)。这里是 官方文档,其中有一个示例,展示了如何在 Map 构造函数中添加数组条目。

当键为 place 时,例如:

[["here", {place: "here",  name: "x", other: "other stuff1" }], ...]

其次,我们将这个修改后的数组传递给 Map 构造函数,这里会发生一些神奇的事情。Map 会消除重复的键值对,仅保留相同键的最后插入的值。 注意:Map 保留插入的顺序。(查看 Map 和对象之间的区别

new Map(上面刚刚映射的条目数组)

第三步我们使用 map 的值来检索原始项,但这次没有重复。

new Map(mappedArr).values()

最后一步是将这些值添加到一个全新的数组中,以便它看起来像最初的结构,并返回该数组:

return [...new Map(mappedArr).values()]


据我所知,创建了一个以属性值为键的Map。但是数组的顺序是否被保留并不是百分之百确定的。 - David Schumann
3
嗨@DavidSchumann,我会更新答案并解释它的工作原理。但简短回答是顺序被保留,第一个被移除...只需考虑它如何插入到映射中...它检查键是否已经存在,如果存在则更新它,因此最后一个将保留。 - V. Sambor
@MiladAbooali,你能给我一个不起作用的例子吗?我会研究一下。谢谢。 - V. Sambor
我通过添加每个项目长度的检查器来解决了这个问题。 - Milad Abooali
8
如果有人需要TS版本,请看这里: ...new Map(arr.map((item: T) => [item[key], item])).values() ];该函数名为"unique",它接收一个数组和一个键名作为参数,并返回一个新的由对象组成的数组。新数组中的每个对象都是通过输入数组中唯一的键值来确定的。该函数利用了JavaScript中的Map数据结构和展开运算符“...”。 - readikus
显示剩余4条评论

249
简单而高效的解决方案,比已经存在的70多个答案具有更好的运行时间。
const ids = arr.map(({ id }) => id);
const filtered = arr.filter(({ id }, index) => !ids.includes(id, index + 1));

示例:

const arr = [{
  id: 1,
  name: 'one'
}, {
  id: 2,
  name: 'two'
}, {
  id: 1,
  name: 'one'
}];

const ids = arr.map(({ id }) => id);
const filtered = arr.filter(({ id }, index) => !ids.includes(id, index + 1));

console.log(filtered);

工作原理:

Array.filter() 通过检查先前映射的 id 数组是否包含当前 id({id} 将对象解构为仅包含其 id)来删除所有重复对象。为了仅过滤出实际的重复项,它使用了 Array.includes() 的第二个参数 fromIndex,并将其设置为 index + 1,这将忽略当前对象和所有之前的对象。

由于每次迭代 filter 回调方法时只会从当前索引 + 1 开始搜索数组,因此这也极大地减少了运行时间,因为只有之前未被过滤的对象才会被检查。

如果没有像 id 这样的唯一标识符怎么办?

只需创建一个临时的标识符:

const objToId = ({ name, city, birthyear }) => `${name}-${city}-${birthyear}`;


const ids = arr.map(objToId);
const filtered = arr.filter((item, index) => !ids.includes(objToId(item), index + 1));

2
@user239558 很好的问题,但实际上不是这样的。它会慢得多,并且对于具有不同顺序的对象,例如 {id: 1, name: 'one'}{namd: 'one', id: 1},它将无法检测到重复项。 - leonheess
4
很好的问题,@Timotronadon。{id}使用解构赋值仅提取对象中的 id 键。为了说明这一点,请看下面两个循环:1.arr.forEach(object => console.log(object.id)) 和 2.arr.forEach({id} => console.log(id))。它们都完成了打印 arr 中所有对象的 id 键的相同任务。然而,一个是使用解构赋值,另一个则使用更常规的通过点符号访问键的方式。 - leonheess
3
这里需要进行最佳反应的定义。简单、干净、优雅,而且非常有效,谢谢! - d0rf47
1
惊人的答案。这个完美地工作,而且没有使用任何外部库。 - SatelBill
1
如果你有这样的东西怎么办? const arr = [{id: 1, name: 'one'}, {id: 2, name: 'two'}, {id: 1, name: 'THREE'}] 而且你不想失去id=1的名称?是否可能将其保留在数组中? - Sonhja
显示剩余9条评论

201
一个基本的方法是:
const obj = {};

for (let i = 0, len = things.thing.length; i < len; i++) {
  obj[things.thing[i]['place']] = things.thing[i];
}

things.thing = new Array();

 for (const key in obj) { 
   things.thing.push(obj[key]);
}

78
不应在for循环中使用length,因为在每次迭代时计算长度会拖慢速度。将其赋值给循环外的变量,然后传递该变量而不是things.thing.length。 - Nosebleed
16
@aefxx,我不太理解这个函数,如果“place”相同但名称不同,您如何处理这种情况?是否应该视为重复项? - Kuan
2
虽然这个方法可以工作,但它不能处理排序数组,因为获取键时顺序不能保证。所以你最终还是要重新排序。现在,假设数组没有排序,但它的顺序很重要,你无法确保顺序保持不变。 - Deepak G M
3
@DeepakGM,你说得完全正确。答案可能不会保留给定的顺序。如果这是一个要求,那么应该寻找另一种解决方案。 - aefxx
如何修改以上代码以删除包含 X 的对象并去重? - Ryan H
显示剩余3条评论

158
如果您可以使用JavaScript库,例如underscore或lodash,我建议查看它们的库中的_.uniq函数。从lodash
_.uniq(array, [isSorted=false], [callback=_.identity], [thisArg])

基本上,您需要传入在此处的对象文字数组并传入要从原始数据数组中删除重复项的属性,例如:

var data = [{'name': 'Amir', 'surname': 'Rahnama'}, {'name': 'Amir', 'surname': 'Stevens'}];
var non_duplidated_data = _.uniq(data, 'name'); 

更新:现在Lodash也引入了.uniqBy方法。


4
@Praveen Pds:我在代码示例中有提到下划线吗?我说的是 'lodash' 有这个功能,而 underscore 也有类似的功能。在投票之前,请仔细阅读答案。 - ambodi
//使用_underscore.js列出唯一的对象 holdingObject = _.uniq(holdingObject, function(item, key, name) { return item.name; }); - praveenpds
39
注意:您现在需要使用uniqBy而不是uniq,例如_.uniqBy(data, 'name')... 文档:https://lodash.com/docs#uniqBy - drmrbrewer
如果你有一个嵌套深度较深的集合:let data = [{'v': {'t':1, 'name':"foo"}}, {'v': {'t':1, 'name':"bar"}}];,可以使用以下代码来进行去重操作: let uniq = _.uniqBy(data, 'v.t'); - Stas Sorokin

98

我有完全相同的需求,即根据单个字段上的重复项,在数组中删除重复对象。我在这里找到了代码:Javascript: Remove Duplicates from Array of Objects

因此,在我的示例中,我正在从数组中删除具有重复licenseNum字符串值的任何对象。

var arrayWithDuplicates = [
    {"type":"LICENSE", "licenseNum": "12345", state:"NV"},
    {"type":"LICENSE", "licenseNum": "A7846", state:"CA"},
    {"type":"LICENSE", "licenseNum": "12345", state:"OR"},
    {"type":"LICENSE", "licenseNum": "10849", state:"CA"},
    {"type":"LICENSE", "licenseNum": "B7037", state:"WA"},
    {"type":"LICENSE", "licenseNum": "12345", state:"NM"}
];

function removeDuplicates(originalArray, prop) {
     var newArray = [];
     var lookupObject  = {};

     for(var i in originalArray) {
        lookupObject[originalArray[i][prop]] = originalArray[i];
     }

     for(i in lookupObject) {
         newArray.push(lookupObject[i]);
     }
      return newArray;
 }

var uniqueArray = removeDuplicates(arrayWithDuplicates, "licenseNum");
console.log("uniqueArray is: " + JSON.stringify(uniqueArray));

结果:

uniqueArray 是:

[{"type":"LICENSE","licenseNum":"10849","state":"CA"},
{"type":"LICENSE","licenseNum":"12345","state":"NM"},
{"type":"LICENSE","licenseNum":"A7846","state":"CA"},
{"type":"LICENSE","licenseNum":"B7037","state":"WA"}]

1
如果该函数能够过滤掉“falsy”对象,那将更加有用。for(var i in array) { if(array[i][prop]){ //valid lookupObject[array[i][prop]] = array[i]; } else { console.log('falsy object'); } } - Abdul Sadik Yalcin
为什么不使用以下代码将复杂度降至O(n)呢:for (let i in originalArray) { if (lookupObject[originalArray[i]['id']] === undefined) { newArray.push(originalArray[i]); } lookupObject[originalArray[i]['id']] = originalArray[i]; } - Tudor B.
这是最好的方法,因为重要的是要知道你不想被复制的是什么。现在,可以通过e6标准的reducer来完成吗? - Christian Matthew

72

使用Set的一行代码

var things = new Object();

things.thing = new Array();

things.thing.push({place:"here",name:"stuff"});
things.thing.push({place:"there",name:"morestuff"});
things.thing.push({place:"there",name:"morestuff"});

// assign things.thing to myData for brevity
var myData = things.thing;

things.thing = Array.from(new Set(myData.map(JSON.stringify))).map(JSON.parse);

console.log(things.thing)

说明:

  1. new Set(myData.map(JSON.stringify)) 使用 myData 元素的字符串形式创建一个Set 对象。
  2. Set对象将确保每个元素都是唯一的。
  3. 然后,我使用Array.from根据创建的set的元素创建一个数组。
  4. 最后,我使用JSON.parse将字符串化的元素转换回对象。

27
问题在于{a:1, b:2}与{b:2, a:1}不相等。 - PirateApp
4
请记住,Date属性可能会出现问题。 - MarkosyanArtur
这行代码使用一个行对象创建了随机的空值,这些空值在原始对象数组中并不存在。你能帮忙吗? - B1K
为了解决@PirateApp在评论中指出的问题,可以修改@Mu提供的答案以处理重新排列属性的对象: const distinct = (data, elements = []) => [...new Set(data.map(o => JSON.stringify(o, elements)))].map(o => JSON.parse(o)); 然后在调用distinct时,只需将属性名称传递给元素数组。对于原始帖子,这将是['place','name']。对于@PirateApp的示例,这将是['a','b'] - knot22

61

ES6一行代码搞定

let arr = [
  {id:1,name:"sravan ganji"},
  {id:2,name:"pinky"},
  {id:4,name:"mammu"},
  {id:3,name:"avy"},
  {id:3,name:"rashni"},
];

console.log(Object.values(arr.reduce((acc,cur)=>Object.assign(acc,{[cur.id]:cur}),{})))


6
如果你只想删除具有单个重复值的对象,那么操作会很简单明了。但是如果对象完全重复,操作就不那么简单了。 - David Barker
1
cur.id]:cur 中的 :cur 功能是什么? 我不明白这段代码的含义。 - Jonathan Arias
使用 lodash(_),我们可以使用 _.uniqBy(arr,'id') 完成相同的事情。 - Akhil S
1
一如既往,代码的解释是很好的。 - Heretic Monkey
这只考虑对象的 id;但是对象键 cur.id 只是对象的一种可能序列化。例如,另一种序列化方式可能是 \${cur.id}-${cur.name}``,这将考虑 两个 属性(尽管通常需要更好的序列化)。不久后,Records 将使其可以轻松地与 Maps 一起使用。 - Sebastian Simon
显示剩余2条评论

46

要从对象数组中删除所有重复项,最简单的方法是使用filter

var uniq = {};
var arr  = [{"id":"1"},{"id":"1"},{"id":"2"}];
var arrFiltered = arr.filter(obj => !uniq[obj.id] && (uniq[obj.id] = true));
console.log('arrFiltered', arrFiltered);


12
在Stack Overflow上,向你的解决方案添加说明,特别是解释为什么你的解决方案会起作用,并且如何比其他答案更好,这是一个很好的做法。有关更多信息,请阅读《如何回答》(//stackoverflow.com/help/how-to-answer)。 - Samuel Liew
1
这不是对原始问题的回答,因为这是在寻找id。该问题需要整个对象在所有字段(例如“place”和“name”)中唯一。 - L. Holanda

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