JavaScript Set 自 ES2015 规范引入以来,一直被视为功能不全。但现在,这一状况即将得到改变。
Sets 是一种集合类型,能够确保其中的值唯一不重复。在 ES2015 版本中,Set 提供的功能主要限于创建、添加、删除元素及检查元素是否属于某个 Set。如果需要对多个集合进行操作或比较,则需要自行编写函数来实现。幸运的是,ECMAScript 规范的制定委员会 TC39 以及各大浏览器开发商已经在这方面取得了进展。目前,我们已经能在 JavaScript 中使用 union(并集)、intersection(交集)和 difference(差集)等操作了。
在深入了解这些新功能之前,让我们先回顾一下现有的 JavaScript Sets 能做什么,然后再探讨下面的新 Set 函数以及支持这些功能的 JavaScript 引擎。
ES2015 版本的 JavaScript Sets 能完成哪些操作?#
通过一些实例来探讨 JavaScript Set 的基本功能是最直接的方法。
你可以创建一个空的 Set,或者通过提供一个可迭代对象(如数组)来初始化一个 Set。
const languages = new Set(["JavaScript", "TypeScript", "HTML", "JavaScript"]);
由于 Set 中的值必须唯一,上述 Set 实际上包含三个元素。可以通过 Set 的 size 属性来确认这一点。
languages.size;
// => 3
使用 add 方法可以向 Set 中添加新元素。如果尝试添加的元素已存在,则不会有任何变化。
languages.add("JavaScript");
languages.add("CSS");
languages.size;
// => 4
可以通过 delete 方法从 Set 中移除元素。
languages.delete("TypeScript");
languages.size;
// => 3
使用 has 方法可以检查某个元素是否属于 Set。与数组相比,Set 在这方面的检查效率更高,因为这一操作的时间复杂度为常数时间(O(1))。
languages.has("JavaScript");
// => true
languages.has("TypeScript");
// => false
你还可以使用 forEach 或 for...of 循环遍历 Set 的元素。元素的排列顺序是按照它们被添加到 Set 中的顺序。
languages.forEach(element => console.log(element));
// "JavaScript"
// "HTML"
// "CSS"
此外,你可以通过 keys、values(实际上和 keys 等价)及 entries 方法从 Set 获取迭代器。
最后,可以使用 clear 方法来清空一个 Set。
languages.clear();
languages.size;
// => 0
这是对使用 ES2015 规范的 Set 可执行操作的简单回顾:
Set提供了处理唯一值集合的方法。- 向
Set添加元素以及测试元素是否存在于Set中都非常高效。 - 将
Array或其他可迭代对象转换为Set是一种简便的去重方法。
然而,这种实现缺少了对 Set 之间进行操作的能力。你可能希望合并两个 Set 以创建一个包含两者所有元素的新 Set(并集)、找出两个 Set 的共同元素(交集),或者确定一个 Set 有而另一个 Set 没有的元素(差集)。直到最近,实现这些操作都需要自定义函数。
新的 Set 函数包括哪些?#
Set 方法的提案为 Set 实例添加了以下方法:union(并集)、intersection(交集)、difference(差集)、symmetricDifference(对称差集)、isSubsetOf(子集判断)、isSupersetOf(超集判断)和 isDisjointFrom(判断是否不相交)。
这些方法中的一些与 SQL 中的某些连接操作相似,我们将通过代码示例来展示每个函数的作用。
以下代码示例可在 Chrome 122+ 或 Safari 17+ 中尝试。
Set.prototype.union(other)#
两个集合的并集是一个包含两个集合中所有元素的集合。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const allLanguages = frontEndLanguages.union(backEndLanguages);
// => Set {"JavaScript", "HTML", "CSS", "Python", "Java"}
在这个例子中,第一个和第二个集合中的所有语言都出现在第三个集合中。与其他向 Set 添加元素的方法一样,重复的元素会被自动去除。
这相当于对两个表执行 SQL 的 FULL OUTER JOIN。

Set.prototype.intersection(other)#
两个集合的交集是一个包含两个集合中共有元素的集合。
const frontEndLanguages
= new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const frontAndBackEnd = frontEndLanguages.intersection(backEndLanguages);
// => Set {"JavaScript"}
这里,“JavaScript” 是唯一同时出现在两个集合中的元素。
交集相当于 SQL 中的 INNER JOIN。

Set.prototype.difference(other)#
操作的集合与另一个集合之间的差集包含了第一个集合独有的所有元素。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.difference(backEndLanguages);
// => Set {"HTML", "CSS"}
const onlyBackEnd = backEndLanguages.difference(frontEndLanguages);
// => Set {"Python", "Java"}
在确定两个集合之间的差异时,调用差集函数的集合和作为参数的集合的顺序非常重要。在上述例子中,从前端语言集合中移除后端语言集合的结果是 “JavaScript” 被去除,留下了 “HTML” 和 “CSS”。反之,从后端语言集合中移除前端语言集合仍然会去除 “JavaScript”,并留下 “Python” 和 “Java”。
差集类似于执行 SQL 中的 LEFT JOIN。

Set.prototype.symmetricDifference(other)#
两个集合的对称差集是一个包含了两个集合中独有的所有元素的集合。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const backEndLanguages = new Set(["Python", "Java", "JavaScript"]);
const onlyFrontEnd = frontEndLanguages.symmetricDifference(backEndLanguages);
// => Set {"HTML", "CSS", "Python", "Java"}
const onlyBackEnd = backEndLanguages.symmetricDifference(frontEndLanguages);
// => Set {"Python", "Java", "HTML", "CSS"}
在这种情况下,尽管结果集中的元素相同,但元素的顺序因所调用的集合不同而有所不同。元素的添加顺序由它们被添加到集合中的顺序决定,而对函数进行操作的集合的元素会首先被添加。
对称差集类似于 SQL 中排除两个表共有元素的 FULL OUTER JOIN。

Set.prototype.isSubsetOf(other)#
如果第一个集合中的所有元素都出现在第二个集合中,则该集合是另一个集合的子集。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSubsetOf(frontEndLanguages);
// => true
frontEndLanguages.isSubsetOf(declarativeLanguages);
// => false
任何集合都是其自身的子集。
frontEndLanguages.isSubsetOf(frontEndLanguages);
// => true
Set.prototype.isSupersetOf(other)#
如果第一个集合包含第二个集合中的所有元素,则该集合是另一个集合的超集。这是成为子集的相反关系。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const declarativeLanguages = new Set(["HTML", "CSS"]);
declarativeLanguages.isSupersetOf(frontEndLanguages);
// => false
frontEndLanguages.isSupersetOf(declarativeLanguages);
// => true
任何集合都是其自身的超集。
frontEndLanguages.isSupersetOf(frontEndLanguages);
// => true
Set.prototype.isDisjointFrom(other)#
如果两个集合没有共同的元素,则它们是不相交的。
const frontEndLanguages = new Set(["JavaScript", "HTML", "CSS"]);
const interpretedLanguages = new Set(["JavaScript", "Ruby", "Python"]);
const compiledLanguages = new Set(["Java", "C++", "TypeScript"]);
interpretedLanguages.isDisjointFrom(compiledLanguages);
// => true
frontEndLanguages.isDisjointFrom(interpretedLanguages);
// => false
在这些例子中,解释型语言和编译型语言的集合没有交集,因此这些集合是不相交的。而前端语言和解释型语言的集合由于共有 “JavaScript” 这一元素,因此它们不是不相交的。
支持情况#
截至本文撰写之时,这些新增的 Set 方法提案在 TC39 的标准制定流程中已进入第 3 阶段,Safari 17(2023 年 9 月发布)和 Chrome 122(2024 年 2 月)已经实现了这些方法。Edge 紧随 Chrome 之后发布,而 Firefox Nightly 已
在实验性标志后支持这些功能,预计这两个浏览器很快也会正式支持。
Bun 同样采用了 Safari 的 JavaScriptCore 引擎,因此已经支持这些新功能。Chrome 对这些功能的支持意味着它们已经被集成到了 V8 JavaScript 引擎中,并且不久将被 Node.js 采纳。
希望这意味着这些提案将顺利过渡到流程的第 4 阶段,并可能及时成为 ES2024 规范的一部分。
Polyfills#
如果你需要在旧版 JavaScript 引擎上获得这些功能的支持,可以使用 polyfills。这些 polyfills 可以通过 core-js 获得,或作为 es-shims 项目中的单独包提供(例如,用于实现并集功能的 set.prototype.union 包)。
如果你已经为这些功能编写了自己的实现,建议首先迁移到这些 polyfills,随着这些功能的广泛支持,逐步淘汰自定义实现。
Sets 的功能不再感觉缺失#
JavaScript Set 长期以来被视为功能不完整,但这七个新函数的加入使其功能更加全面。将这类功能内建于语言本身,意味着我们可以减少对外部依赖或自行实现的需求,更专注于解决实际问题。
这只是 TC39 当前审议的众多第 3 阶段提案中的一部分。查看这份列表,了解 JavaScript 接下来可能加入的新功能。我特别关注 Temporal 和 Decorators,这两项提案可能会改变我们编写 JavaScript 的重要部分的方式。