比较ECMA6集合的相等性

211

你如何比较两个 JavaScript 集合?我尝试使用 =====,但两者都返回 false。

a = new Set([1,2,3]);
b = new Set([1,3,2]);
a == b; //=> false
a === b; //=> false

这两组是等价的,因为根据定义,集合没有顺序(至少通常情况下是这样)。我查看了MDN 上 Set 的文档,但没有找到有用的信息。有人知道怎么做吗?


那么你如何比较它们呢? - williamcodes
3
迭代并比较每个成员的值,如果全部相同,则集合为“相同”。 - dandavis
1
@dandavis,使用集合时,成员就是值。 - user663031
4
集合(Set)和映射(Map)确实有一个顺序,那就是插入的顺序 - 无论出于什么原因: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries - CodeManX
23
最糟糕的是,即使是new Set([1,2,3]) != new Set([1,2,3])。这使得JavaScript中的 Set 对于“集合的集合”而言毫无用处,因为超级集合将包含重复的子集。唯一想到的解决方法是将所有子集转换为数组,对每个数组进行排序,然后将每个数组编码为字符串(例如JSON)。 - 7vujy0f0hy
显示剩余3条评论
18个回答

155

试试这个:

const eqSet = (xs, ys) =>
    xs.size === ys.size &&
    [...xs].every((x) => ys.has(x));

const ws = new Set([1, 2, 3]);
const xs = new Set([1, 3, 2]);
const ys = new Set([1, 2, 4]);
const zs = new Set([1, 2, 3, 4]);

console.log(eqSet(ws, xs)); // true
console.log(eqSet(ws, ys)); // false
console.log(eqSet(ws, zs)); // false


2
我认为你应该将 has 的名称更改为 isPartOfisInelem - Bergi
1
@DavidGiven 是的,在JavaScript中,集合按照插入顺序进行迭代:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Set/values - Aadit M Shah
140
我越来越确信JavaScript是有史以来最糟糕的语言。每个人都必须发明自己的基本函数来应对其限制,这还是在ES6中,而我们现在已经是2017年了!为什么他们不能将这些常用的函数添加到Set对象的规范中呢? - Ghasan غسان
2
@GhasanAl-Sakkaf,我同意,TC39也许由科学家组成,但没有实用主义者... - Marecky
1
@AaditMShah 谢谢,但不幸的是我在一个禁止使用linting的环境中工作,否则会破坏公司内部node_modules包。我找到了另一种解决方法 > const x of Array.from(s1.values()) 并使用 // eslint-disable-next-line no-restricted-syntax。最终,我仍然认为这是一个糟糕的解决方案,因为它在比较期间进行了o(n)查找,这违背了使用集合和其具有的o(1)能力的目的。正如Ghasan所说,我真的希望javascript有一个.equals()来自于Set() :( - Fiddle Freak
显示剩余9条评论

121

你也可以尝试:

const a = new Set([1,2,3]);
const b = new Set([1,3,2]);

const areSetsEqual = (a, b) => a.size === b.size && [...a].every(value => b.has(value));

console.log(areSetsEqual(a,b)) 


绝对是更好的解决方案,因为它适合于if条件中。 - Hugodby
我喜欢这个解决方案的惯用性和可读性!感谢@Max。 - daydreamer
1
谢谢你让我发现了Array.every。但是由于它不会修改a,为什么需要使用[...a]来复制呢? - ouk
4
every() 是数组 API 的一部分,它不存在于 Set 或 Iterator 中。 - Max Leizerovich
很棒的解决方案,@MaxLeizerovich!我对JavaScript还很陌生,但似乎Array方法比Set更强大。使用JavaScript的函数式编程部分编写集合相等性也是不错的选择。 - Hilton Fernandes
从复杂度的角度来看,集合(Set)要高效得多。 - Max Leizerovich

62

lodash 提供_.isEqual(),可以进行深度比较。如果您不想编写自己的比较函数,这非常方便。 从lodash 4开始,_.isEqual()能够正确比较Sets。

const _ = require("lodash");

let s1 = new Set([1,2,3]);
let s2 = new Set([1,2,3]);
let s3 = new Set([2,3,4]);

console.log(_.isEqual(s1, s2)); // true
console.log(_.isEqual(s1, s3)); // false

18

您可以执行以下操作:

const a = new Set([1,2,3]);
const b = new Set([1,3,2]);

// option 1
console.log(a.size === b.size && new Set([...a, ...b]).size === a.size)

// option 2
console.log([...a].sort().join() === [...b].sort().join())


1
请注意,您可能希望使用“特殊”分隔符进行join操作,以确保集合的元素不包含分隔符。例如,当使用逗号(默认)作为分隔符时,new Set(["1,2","3"])将等于new Set([1,2,3]) - Sebastian
new Set([...a, ...b]) 真是太厉害了 :D - Matmarbon
不过我想知道那个的性能如何。 - Matmarbon

15

这些解决方案都不能将期望的功能带回到像集合中的集合这样的数据结构中。在当前状态下,Javascript的Set对于此目的毫无用处,因为超集将包含重复的子集,而Javascript错误地将其视为不同。我能想到的唯一解决方案是将每个子集转换为Array,进行排序,然后编码为String(例如JSON)。

解决方法

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

基本用法

var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var fromJsonSet = jset => new Set(JSON.parse(jset));

var [s1,s2] = [new Set([1,2,3]), new Set([3,2,1])];
var [js1,js2] = [toJsonSet([1,2,3]), toJsonSet([3,2,1])]; // even better

var r = document.querySelectorAll("td:nth-child(2)");
r[0].innerHTML = (toJsonSet(s1) === toJsonSet(s2)); // true
r[1].innerHTML = (toJsonSet(s1) == toJsonSet(s2)); // true, too
r[2].innerHTML = (js1 === js2); // true
r[3].innerHTML = (js1 == js2); // true, too

// Make it normal Set:
console.log(fromJsonSet(js1), fromJsonSet(js2)); // type is Set
<style>td:nth-child(2) {color: red;}</style>

<table>
<tr><td>toJsonSet(s1) === toJsonSet(s2)</td><td>...</td></tr>
<tr><td>toJsonSet(s1) == toJsonSet(s2)</td><td>...</td></tr>
<tr><td>js1 === js2</td><td>...</td></tr>
<tr><td>js1 == js2</td><td>...</td></tr>
</table>

最终测试:集合的集合

var toSet = arr => new Set(arr);
var toJsonSet = aset /* array or set */ => JSON.stringify([...new Set(aset)].sort()); 
var toJsonSet_WRONG = set => JSON.stringify([...set]); // no sorting!

var output = document.getElementsByTagName("code"); 
var superarray = [[1,2,3],[1,2,3],[3,2,1],[3,6,2],[4,5,6]];
var superset;

Experiment1:
    superset = toSet(superarray.map(toSet));
    output[0].innerHTML = superset.size; // incorrect: 5 unique subsets
Experiment2:
    superset = toSet([...superset].map(toJsonSet_WRONG));
    output[1].innerHTML = superset.size; // incorrect: 4 unique subsets
Experiment3:
    superset = toSet([...superset].map(toJsonSet));
    output[2].innerHTML = superset.size; // 3 unique subsets
Experiment4:
    superset = toSet(superarray.map(toJsonSet));
    output[3].innerHTML = superset.size; // 3 unique subsets
code {border: 1px solid #88f; background-color: #ddf; padding: 0 0.5em;}
<h3>Experiment 1</h3><p>Superset contains 3 unique subsets but Javascript sees <code>...</code>.<br>Let’s fix this... I’ll encode each subset as a string.</p>
<h3>Experiment 2</h3><p>Now Javascript sees <code>...</code> unique subsets.<br>Better! But still not perfect.<br>That’s because we didn’t sort each subset.<br>Let’s sort it out...</p>
<h3>Experiment 3</h3><p>Now Javascript sees <code>...</code> unique subsets. At long last!<br>Let’s try everything again from the beginning.</p>
<h3>Experiment 4</h3><p>Superset contains 3 unique subsets and Javascript sees <code>...</code>.<br><b>Bravo!</b></p>


1
很棒的解决方案!如果你知道你只有一组字符串或数字,那么它就变成了 [...set1].sort().toString() === [...set2].sort().toString() - user993683
1
很不幸,我现在没有时间来审核这个,但是大多数使用内置默认值即.sort()对js集合键进行排序的解决方案都是错误的,因为js对象上不存在完全顺序,例如NaN!= NaN,'2'<3(强制转换),等等。 - ninjagecko

10

我认为这是最高效的版本,因为它不会创建新数组,而是使用Set自己的迭代器:

function isEqualSets(a, b) {
  if (a === b) return true;
  if (a.size !== b.size) return false;
  for (const value of a) if (!b.has(value)) return false;
  return true;
}

这里唯一不会创建一个包含所有集合成员的辅助数组的解决方案。 - vbraun
这个不检查顺序。 - Matthias
2
正如原问题所解释的那样,检查顺序并不是必要的。 - galatians

8
另一个答案也可以,这里提供另一种替代方案。
// Create function to check if an element is in a specified set.
function isIn(s)          { return elt => s.has(elt); }

// Check if one set contains another (all members of s2 are in s1).
function contains(s1, s2) { return [...s2] . every(isIn(s1)); }

// Set equality: a contains b, and b contains a
function eqSet(a, b)      { return contains(a, b) && contains(b, a); }

// Alternative, check size first
function eqSet(a, b)      { return a.size === b.size && contains(a, b); }

但要注意,这并不进行深度相等比较。所以

eqSet(Set([{ a: 1 }], Set([{ a: 1 }])

将返回false。如果要将上述两个集合视为相等,则需要遍历两个集合并对每个元素进行深度质量比较。我们假定存在一个deepEqual例程。然后逻辑将是:

// Find a member in "s" deeply equal to some value
function findDeepEqual(s, v) { return [...s] . find(m => deepEqual(v, m)); }

// See if sets s1 and s1 are deeply equal. DESTROYS s2.
function eqSetDeep(s1, s2) {
  return [...s1] . every(a1 => {
    var m1 = findDeepEqual(s2, a1);
    if (m1) { s2.delete(m1); return true; }
  }) && !s2.size;
}

这个函数的作用是:对于s1中的每个成员,查找一个与之深度相等的s2成员。如果找到了,则删除它,以便不能再次使用。当且仅当s1中的所有元素都在s2中被找到并且s2已经用完时,两个集合才被认为是深度相等的。该功能未经测试。
您可能会发现这个链接有用:http://www.2ality.com/2015/01/es6-set-operations.html

4
如果集合中只包含基本数据类型或者其中对象具有引用相等性,则可以有更简单的方法。
const isEqualSets = (set1, set2) => (set1.size === set2.size) && (set1.size === new Set([...set1, ...set2]).size);

2
使用“==”或“===”运算符比较两个对象时,除非这两个对象引用同一个对象,否则始终会得到false。例如:
var a = b = new Set([1,2,3]); // NOTE: b will become a global variable
a == b; // <-- true: a and b share the same object reference

否则,即使对象包含相同的值,== 也等同于 false:
var a = new Set([1,2,3]);
var b = new Set([1,2,3]);
a == b; // <-- false: a and b are not referencing the same object

您需要考虑手动比较。
在ECMAScript 6中,您可以先将集合转换为数组,以便您可以发现它们之间的差异。
function setsEqual(a,b){
    if (a.size !== b.size)
        return false;
    let aa = Array.from(a); 
    let bb = Array.from(b);
    return aa.filter(function(i){return bb.indexOf(i)<0}).length==0;
}

注意:`Array.from` 是 ECMAScript 6 的标准功能之一,但在现代浏览器中并不广泛支持。请查看此处的兼容性表格:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from#Browser_compatibility

1
这不会无法识别b中不在a中的成员吗? - user663031
1
@torazaburo 的确。跳过检查 b 的成员是否不在 a 中的最佳方法是检查 a.size === b.size - Aadit M Shah
1
如果不必要,先使用 a.size === b.size 进行短路比较,避免对单个元素进行比较。 - user663031
2
如果大小不同,根据定义,集合不相等,因此最好先检查该条件。 - user663031
1
另一个问题在于,由于集合的本质,集合上的“has”操作被设计为非常高效,而数组上的“indexOf”操作则不是。因此,将您的过滤函数更改为“return!b.has(i)”是有意义的。这也消除了将“b”转换为数组的需要。 - user663031
显示剩余8条评论

2

根据被接受的答案,假设支持Array.from,这里是一个一行代码的解决方案:

function eqSet(a, b) {
    return a.size === b.size && Array.from(a).every(b.has.bind(b));
}

或者使用箭头函数和展开运算符的真正一行代码: eqSet = (a,b) => a.size === b.size && [...a].every(b.has.bind(b)) - John Hoffer

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