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

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个回答

35

使用Map进行单行操作(高性能,不保留顺序)

在数组arr中查找唯一的id

const arrUniq = [...new Map(arr.map(v => [v.id, v])).values()]

如果顺序很重要,请查看使用过滤器的解决方案:使用过滤器的解决方案


在数组arr中通过多个属性(placename)进行唯一筛选。

const arrUniq = [...new Map(arr.map(v => [JSON.stringify([v.place,v.name]), v])).values()]

在数组 arr 中,所有属性唯一。

const arrUniq = [...new Map(arr.map(v => [JSON.stringify(v), v])).values()]

保留数组 arr 中的第一个出现。

const arrUniq = [...new Map(arr.slice().reverse().map(v => [v.id, v])).values()].reverse()

多属性解决方案完美地运作了。非常感谢! - CodeSammich
高性能? - leonheess
“高性能”一定是个笑话,因为这根本不快。 - leonheess

30

如果您只需要按对象的一个字段进行比较,则可以使用数组迭代方法来完成另一种选项:

    function uniq(a, param){
        return a.filter(function(item, pos, array){
            return array.map(function(mapItem){ return mapItem[param]; }).indexOf(item[param]) === pos;
        })
    }

    uniq(things.thing, 'place');

尽管这个算法的时间复杂度大于O(n²),但它适合我的使用情况,因为我的数组大小始终小于30。谢谢! - Sterex

26

这是一种通用的方法:您传递一个测试数组中两个元素是否被视为相等的函数。在本例中,它比较正在比较的两个对象的nameplace属性的值。

ES5 答案

function removeDuplicates(arr, equals) {
    var originalArr = arr.slice(0);
    var i, len, val;
    arr.length = 0;

    for (i = 0, len = originalArr.length; i < len; ++i) {
        val = originalArr[i];
        if (!arr.some(function(item) { return equals(item, val); })) {
            arr.push(val);
        }
    }
}

function thingsEqual(thing1, thing2) {
    return thing1.place === thing2.place
        && thing1.name === thing2.name;
}

var things = [
  {place:"here",name:"stuff"},
  {place:"there",name:"morestuff"},
  {place:"there",name:"morestuff"}
];

removeDuplicates(things, thingsEqual);
console.log(things);

原始 ES3 答案

function arrayContains(arr, val, equals) {
    var i = arr.length;
    while (i--) {
        if ( equals(arr[i], val) ) {
            return true;
        }
    }
    return false;
}

function removeDuplicates(arr, equals) {
    var originalArr = arr.slice(0);
    var i, len, j, val;
    arr.length = 0;

    for (i = 0, len = originalArr.length; i < len; ++i) {
        val = originalArr[i];
        if (!arrayContains(arr, val, equals)) {
            arr.push(val);
        }
    }
}

function thingsEqual(thing1, thing2) {
    return thing1.place === thing2.place
        && thing1.name === thing2.name;
}

removeDuplicates(things.thing, thingsEqual);

1
即使两个对象具有相同的属性和值,它们也不会被评估为相等。 - kennebec
是的,我知道。但说得好,我没有正确阅读问题:我没有注意到他需要清除具有相同属性的对象。我会编辑我的答案。 - Tim Down
1
使用Array.prototype.some方法替代数组包含内的while循环 如果数组中有一个成员满足条件,则返回true - MarkosyanArtur

25
如果你可以等到所有添加完成后再消除重复项,通常的做法是先对数组进行排序,然后消除重复项。排序避免了在遍历元素时扫描数组的 N * N 方法。
“消除重复项”函数通常称为 uniqueuniq。一些现有的实现可能会将这两个步骤合并,例如 prototype's uniq这篇文章提供了一些尝试的想法(以及一些要避免的 :-) ),如果你的库中没有一个!就我个人而言,我认为这是最直接的方法:
    function unique(a){
        a.sort();
        for(var i = 1; i < a.length; ){
            if(a[i-1] == a[i]){
                a.splice(i, 1);
            } else {
                i++;
            }
        }
        return a;
    }  

    // Provide your own comparison
    function unique(a, compareFunc){
        a.sort( compareFunc );
        for(var i = 1; i < a.length; ){
            if( compareFunc(a[i-1], a[i]) === 0){
                a.splice(i, 1);
            } else {
                i++;
            }
        }
        return a;
    }

那对于没有自然排序的通用对象是行不通的。 - Tim Down
是的,我添加了一个用户提供的比较版本。 - maccullt
你提供的比较函数无法工作,因为如果你的比较函数是 function(_a,_b){return _a.a===_b.a && _a.b===_b.b;},则数组不会被排序。 - graham.reeds
1
这是一个无效的比较函数。来自https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/sort... function compare(a, b) { if (a按某个排序标准小于b) return -1; if (a按排序标准大于b) return 1; // a必须等于b return 0; } ... - maccullt

22

我认为最好的方法是使用 reduceMap 对象这是一种单行解决方案。

const data = [
  {id: 1, name: 'David'},
  {id: 2, name: 'Mark'},
  {id: 2, name: 'Lora'},
  {id: 4, name: 'Tyler'},
  {id: 4, name: 'Donald'},
  {id: 5, name: 'Adrian'},
  {id: 6, name: 'Michael'}
]

const uniqueData = [...data.reduce((map, obj) => map.set(obj.id, obj), new Map()).values()];

console.log(uniqueData)

/*
  in `map.set(obj.id, obj)`
  
  'obj.id' is key. (don't worry. we'll get only values using the .values() method)
  'obj' is whole object.
*/


3
通过删除行之间的回车和/或换行符,任何东西都可以成为“单行解决方案” :P。 - Heretic Monkey

17

考虑使用lodash.uniqWith

const objects = [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }, { 'x': 1, 'y': 2 }];
 
_.uniqWith(objects, _.isEqual);
// => [{ 'x': 1, 'y': 2 }, { 'x': 2, 'y': 1 }]

1
既然lodash的uniq和uniqBy都没能解决问题,但是你的解决方案却做到了。谢谢!不过如果你的代码是直接复制的,请注明出处。https://lodash.com/docs/4.17.10#uniqWith - Manu CJ
这个解决方案对我来说完美运作。 - Mamé

16

再加一个方法,使用ES6和Array.reduce以及Array.find。在这个例子中,按照guid属性过滤对象。

let filtered = array.reduce((accumulator, current) => {
  if (! accumulator.find(({guid}) => guid === current.guid)) {
    accumulator.push(current);
  }
  return accumulator;
}, []);

将这个扩展程序进一步完善,使其能够选择属性并将其压缩成一行:

const uniqify = (array, key) => array.reduce((prev, curr) => prev.find(a => a[key] === curr[key]) ? prev : prev.push(curr) && prev, []);

要使用它,请传递一个对象数组和您希望基于其进行去重的关键字名称作为字符串值:

const result = uniqify(myArrayOfObjects, 'guid')

15

let myData = [{place:"here",name:"stuff"}, 
 {place:"there",name:"morestuff"},
 {place:"there",name:"morestuff"}];


let q = [...new Map(myData.map(obj => [JSON.stringify(obj), obj])).values()];

console.log(q)

使用ES6和new Map()创建一行代码。

// assign things.thing to myData
let myData = things.thing;

[...new Map(myData.map(obj => [JSON.stringify(obj), obj])).values()];

详情:

  1. 对数据列表进行 .map() 操作,将每个单独的对象转换为一个 [key, value] 对数组(长度为2),第一个元素(键)将是对象的 stringified 版本,第二个元素(值)将是一个对象本身。
  2. 将上面创建的数组列表添加到 new Map() 中,它的键将是 stringified 对象,并且任何相同键的添加都会覆盖已经存在的键。
  3. 使用 .values() 将给出包含 Map 中所有值的 MapIterator(在我们的情况下是 obj)。
  4. 最后,使用 spread ... 运算符从上一步生成新的数组,其中包含这些值。

15

宝贝们,咱们把这玩意儿弄碎吧,怎么样?

let uniqIds = {}, source = [{id:'a'},{id:'b'},{id:'c'},{id:'b'},{id:'a'},{id:'d'}];
let filtered = source.filter(obj => !uniqIds[obj.id] && (uniqIds[obj.id] = true));
console.log(filtered);
// EXPECTED: [{id:'a'},{id:'b'},{id:'c'},{id:'d'}];


1
这并没有回答原始问题,因为这是在搜索“id”。该问题需要整个对象在所有字段(如“place”和“name”)上都是唯一的。 - L. Holanda
2
这是对问题的上述概括的进一步完善。原始问题是9年前发布的,因此原始发布者可能今天不再担心“place”和“name”。阅读此线程的任何人都在寻找一种最佳方法来去重对象列表,而这是一种紧凑的方法。 - Cliff Hall

15
您也可以使用一个Map
const dedupThings = Array.from(things.thing.reduce((m, t) => m.set(t.place, t), new Map()).values());

完整示例:

const 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"});

const dedupThings = Array.from(things.thing.reduce((m, t) => m.set(t.place, t), new Map()).values());

console.log(JSON.stringify(dedupThings, null, 4));

结果:

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

+1,不过如果能更详细地解释dedupThings的内部工作原理就更好了——好处是我现在明白了reduce:D - MimiEAM
1
太棒了,终于看到 Map 的用法啦 :D - Farhad

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