通过多个属性筛选的Underscore/Lodash去重函数

50

我有一个包含重复数据的对象数组,我想要得到一个唯一的列表,其中唯一性是由对象的某些属性子集定义的。例如,

{a:"1",b:"1",c:"2"}

我希望在唯一性比较中忽略掉c

可以像这样做:

_.uniq(myArray,function(element) { return element.a + "_" + element+b});
我希望我能做到
_.uniq(myArray,function(element) { return {a:element.a, b:element.b} });

但那样不行。我能做类似的事情吗,还是说如果我要比较多个属性,需要创建一个可比较的对象?


你为什么要尝试第二次尝试?第一次已经成功了,对吧? - friedi
1
是的,第一个方法可以工作,但是需要进行字符串拼接,感觉有点不太自然。尝试理解是否有更自然的方法来完成这个任务。 - Jeff Storey
对象始终是唯一的,因此您需要按照各个属性值进行比较,而不是整个对象进行比较。使用字符串比较可以处理某些数据,但不能处理其他数据,例如:对于像所示的数字字符串,您会冒着将 {a:"1"} 与 {a:1} 碰撞的风险。 - dandavis
在我的特定情况下,我只比较字符串。@dandavis 我不想比较所有属性,只想比较其中的一部分。 - Jeff Storey
@JeffStorey 你是想严格使用 _unique,还是想要一个更加功能性的解决方案?比如创建一个比较函数并结合 reduce/find 或者结合 filter/find - Koushik Chatterjee
显示剩余2条评论
6个回答

51

使用 Lodash 的 uniqWith 方法:

_.uniqWith(array, [comparator])

该方法类似于 _.uniq,但它接受一个 comparator 函数,用于比较数组中的元素。结果值的顺序由它们在数组中出现的顺序决定。比较函数接受两个参数: (arrVal, othVal)

当比较函数返回 true 时,被视为重复项的元素将只包含在新数组的第一次出现中。


例子:
我有一个地点列表,其中包含纬度和经度坐标 - 其中一些是相同的 - 并且我想查看具有唯一坐标的位置列表:

const locations = [
  {
    name: "Office 1",
    latitude: -30,
    longitude: -30
  },
  {
    name: "Office 2",
    latitude: -30,
    longitude: 10
  },
  {
    name: "Office 3",
    latitude: -30,
    longitude: 10
  }
];

const uniqueLocations = _.uniqWith(
  locations,
  (locationA, locationB) =>
    locationA.latitude === locationB.latitude &&
    locationA.longitude === locationB.longitude
);

// Result has Office 1 and Office 2

6
这应该就是答案。 - eddy
这个可以工作,但是函数内部需要一个Return。 - Franco
@Franco 箭头函数使用隐式的 return在这里阅读更多 - Reed Dunkle
复杂度怎么样? 我不确定,但是可能 uniqWith 的复杂度为 O(n^2),而 uniqBy 的复杂度为 O(n)。 - Stanislau Listratsenka
@StanislauListratsenka 我认为它们是两个不同的工具,用于两个不同的目的。如果你只需要 uniqBy,那我会使用它。在我看来,这种情况更适合使用 uniqWith。我不确定复杂度。乍一看,似乎应该是2N,而不是n²,这与使用 uniqBy 的解决方案相同。但我不是100%确定。如果您在源代码中发现了什么,请告诉我。 - Reed Dunkle

43
很遗憾,似乎没有简单的方法来做到这一点。除非你自己编写一个函数,否则你需要返回可以直接进行相等比较的内容(与你第一个示例中的相同)。其中一种方法是只需.join()所需的属性:
_.uniqBy(myArray, function(elem) { return [elem.a, elem.b].join(); });

你也可以使用_.pick_.omit来移除不需要的内容。从那里开始,您可以使用_.values并带有.join(),甚至只需使用JSON.stringify

_.uniqBy(myArray, function(elem) {
    return JSON.stringify(_.pick(elem, ['a', 'b']));
});

记住对象的属性顺序并不是确定性的,所以你可能希望坚持使用显式数组方法。

P.S. 对于 Lodash < 4,请用 uniq 替换 uniqBy


7
使用join('')存在诸多漏洞(例如[1,23][12,3])。 - mu is too short
@muistooshort:说得好 - 我想标准的逗号分隔连接会更好。 - voithos
1
@voithos:但是如果数据包含逗号怎么办?或者数字是否被引用? - dandavis
@dandavis:我想你可以对数组使用JSON.stringify,而不是使用join。但实际上,更容易的方法是重写uniq函数以考虑多个属性。 - voithos
11
lodash 4版本提供了_.uniqWith(myArray, _.isEqual)函数,用于去除数组中的重复项,并基于值使用深度比较来确定哪些项是重复的。 - nils petersohn
显示剩余2条评论

10

2

虽然有点晚了,但我在lodash文档中找到了这个。

var 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
这是我个人认为最优雅的解决方案。这应该被标记为最佳答案。 - Bugs Bunny

1
我认为join()方法仍然是最简单的方法。尽管在之前的解决方案中提出了一些问题,但我认为选择正确的分隔符是避免已识别陷阱(具有不同值集返回相同连接值)的关键。请记住,分隔符不必是单个字符,它可以是任何您确信不会在数据本身中自然出现的字符串。我经常这样做,并喜欢使用“~!$〜”作为我的分隔符。它还可以包括特殊字符,如\t\r\n等。
如果数据确实是不可预测的,也许最大长度是已知的,您可以在连接之前将每个元素填充到其最大长度。

1
在 @voithos 和 @Danail 的结合回答中有一个提示。我解决这个问题的方法是在我的数组对象上添加一个唯一键。
起始示例数据
const animalArray = [
  { a: 4, b: 'cat', d: 'generic' },
  { a: 5, b: 'cat', d: 'generic' },
  { a: 4, b: 'dog', d: 'generic' },
  { a: 4, b: 'cat', d: 'generic' },
];

在上面的示例中,我希望数组通过 ab 是唯一的,但现在有两个对象具有 a: 4b: 'cat'。通过将 a + b 组合成字符串,我可以得到一个唯一的键来进行检查。
{ a: 4, b: 'cat', d: 'generic', id: `${a}-${b}` }. // id is now '4-cat'

注意:显然你需要在数据上进行映射或在对象创建时执行此操作,因为你无法在同一对象内引用对象的属性。
现在比较就很简单了...
_.uniqBy(animalArray, 'id');

生成的数组长度为3,它将删除最后一个重复项。

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