本文翻译自 Union, intersection, difference, and more are coming to JavaScript Sets,作者:Phil Nash, 略有删改。

JavaScript Set 在 ES2015 规范中引入,但一直感觉不完整。

Set 是一种值的集合,其中每个值只能出现一次。在 ES2015 版本的 Set 中,可用的功能围绕创建、添加、移除以及检查 Set 的成员资格。如果你想操作或比较多个集合,需要自己实现相关逻辑。幸运的是,负责处理 ECMAScript 规范的 TC39 委员会和浏览器一直在努力解决这个问题。我们现在在 JavaScript 实现中看到了 unionintersectiondifference 等函数。

在我们查看新功能之前,先来回顾一下现在的 Set 现在能做什么,然后我们将深入了解新的 Set 函数以及支持它们的 JavaScript 引擎。

ES2015 Set 能做什么?

让我们看看 ES2015 版本的 Set 能做什么。

你可以不带任何参数构造一个 Set,这会给你一个空的 Set。或者你可以提供一个可迭代对象,如数组,来初始化 Set

const languages = new Set(["JavaScript", "TypeScript", "HTML", "JavaScript"]);

Set 只能包含唯一值,所以上面的 Set 最终只有三个值。你可以用 size 属性检查这一点。

languages.size;
// => 3

你可以用 add 函数向 Set 添加更多元素。如果 Set 中已存在要添加的元素,则不执行任何操作。

languages.add("JavaScript");
languages.add("CSS");
languages.size;
// => 4

你可以使用 deleteSet 中移除元素。

languages.delete("TypeScript");
languages.size;
// => 3

你可以使用 has 函数检查元素是否是 Set 的成员。Set 的一个好处是这个检查可以在常数时间内完成(O(1)),而检查元素是否在 Array 中的时间则取决于 Array 的长度(O(n))。对于这样的任务,使用 Set 集合是一种编写高效代码的好方法。

languages.has("JavaScript");
// => true
languages.has("TypeScript");
// => false

你可以使用 forEachfor...of 循环遍历 Set 的元素。元素按它们被添加到 Set 的顺序排序。

languages.forEach(element => console.log(element));
// "JavaScript"
// "HTML"
// "CSS"

你还可以使用 keysvalues 函数(实际上是等效的)以及 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 方法提案为 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" 是这里两个集合中都存在的唯一元素。

交集就像是一个 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"。

差集就像执行一个 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"}

在这种情况下,结果集中的元素是相同的,但请注意顺序不同。集合的顺序由元素添加到集合的顺序决定,执行函数的集合的元素将首先被添加。

对称差集就像一个排除了两个表中都存在的元素的 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" 这个共同元素,所以它们不是不相交的。

支持情况

截至撰写本文时,该提案在 TC39 的过程中处于第 3 阶段,Safari 17(2023年9月发布)和 Chrome 122(2024年2月)已经发布了这些方法的实现。Edge 紧随 Chrome,Firefox Nightly 也有支持,我预计这两种浏览器很快也会支持。

希望这意味着该提案将进入该过程的第 4 阶段,甚至可能及时加入 ES2024 规范。

Polyfills

虽然你需要旧的 JavaScript 引擎支持,但可以使用polyfill来升级这些函数的规范兼容实现。它们在 core-js 中可用,或者作为每个函数的单独包在 es-shims 项目中(例如,set.prototype.union 包可用于联合功能)。

Set 不再感觉不完整

长期以来,JavaScript Set 一直感觉不完整,但这些 7 个新函数很好地完善了实现。将这样的功能使用到开发中意味着我们将减少很多相关依赖。


看完本文如果觉得有用,记得点个赞支持,收藏起来说不定哪天就用上啦~

专注前端开发,分享前端相关技术干货,公众号:南城大前端(ID: nanchengfe)