大家好,这里是大家的林语冰。
JS 中的 Set
(集合)在 ES2015 规范中首次引入,但它一直美中不足。这种不上不下的现状即将改变。
Set
是值的集合,其中每个值能且仅能出现一次。在 Set
的 ES2015 版本中,实用功能主要围绕创建、添加、删除以及检查 Set
的成员。如果我们想对多个集合进行操作或比较,那就必须自己编写工具函数。幸运的是,TC39 委员会(为制定 ECMAScript 规范而成立的委员会)和浏览器一直在致力于此。我们现在可以在 JS 提案中看到诸如 union
、intersection
和 difference
之类的全新函数。
在了解新型功能之前,我们先来温习一下 JS 中 Set
目前的功能,然后我们会在下文介绍全新的 Set
函数,以及已经支持的 JS 引擎。
免责声明
本文属于是语冰的直男翻译了属于是,略有删改,仅供粉丝参考。英文原味版请传送 Union, intersection, difference, and more are coming to JavaScript Sets。
ES2015 的 JS Set
我们来瞄一下 ES2015 版本的 JS Set
能做什么。
我们可以构造一个没有任何参数的 Set
,这会返回一个空集。或者,我们可以提供一个数组之类的可迭代对象来初始化 Set
。
js
const languages = new Set(['JS', 'TS', 'HTML', 'JS'])
Set
能且仅能包含唯一值,因此虽然我们在上面传递了 4 个元素,但 Set
有且仅有 3 个不会重复的唯一成员。我们可以使用 size
属性来证明。
js
languages.size // 3
我们可以使用 add
函数向 Set 添加更多元素。添加 Set
中已存在的元素则不会执行任何操作。
js
languages.add('JS')
languages.add('CSS')
languages.size // 4
我们可以使用 delete
从 Set
中删除元素。
js
languages.delete('TS')
languages.size // 3
我们还可以使用 has
函数检查某个元素是否是 Set
的成员。Set
的好处之一在于,这种检查可以在常数时间复杂度 O(1) 内完成,而检查元素是否在 Array
中需要根据 Array
的长度在线性时间复杂度 O(n) 内完成。使用 Set
来完成这样的任务是一种有意编写高效代码的精简方法。
js
languages.has('JS') // true
languages.has('TypeScript') // false
我们可以使用 forEach
或 for...of
循环遍历 Set
的元素。元素按照添加到 Set
的顺序排序。
js
languages.forEach(element => console.log(element))
// "JS"
// "HTML"
// "CSS"
我们还可以使用等价的 keys
和 values
函数,以及 entries
从 Set
获取迭代器功能。
最后,我们可以使用 clear
函数重置清空 Set
。
js
languages.clear()
languages.size // 0
综上所述,我们看到了可以使用 ES2015 规范版本的 Set
做什么:
Set
提供了处理唯一值集合的方法- 将元素添加到
Set
,并测试它们在 Set 中的存在是有效的 - 将
Array
或其他可迭代对象转换为Set
是过滤重复元素的简单方法
不过,ES2015 的实现错过了 Set
集合之间的操作。我们可能想要创建一个 Set
,其中包含来自其他两个 Set
的所有元素,即两个 Set
的并集;或者找出两个 Set
具有的共同点元素,即交集;抑或是找出一个集合中不存在、但在另一个集合中存在的元素,即差集。直到最近,我们还必须自己实现相应的工具函数。
全新的 Set
功能有哪些?
Set
方法提案将以下方法添加到 Set
实例中:
union
intersection
difference
symmetricDifference
isSubsetOf
isSupersetOf
isDisjointFrom
其中某些方法类似于某些数据库操作,我们将用它们来说明对应代码的结果。让我们瞄一下每个函数对应功能的若干示例。
您可以在 Chrome 122+ 或 Safari 17+ 中体验下文的任何代码示例。
Set.prototype.union(other)
Set
的并集是包含任一集合中存在的所有元素的集合。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const backEndLanguages = new Set(['Go', 'JS'])
const allLanguages = frontEndLanguages.union(backEndLanguages)
// => Set {"JS", "HTML", "CSS", "Go"}
在此示例中,前两个集合中的所有语言都在第三个并集中。与向 Set
添加元素的其他方法一样,重复元素会被删除。
这相当于两个表之间的 SQL FULL OUTER JOIN
。
Set.prototype.intersection(other)
交集是包含两个集合中同时存在的所有元素的集合。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const backEndLanguages = new Set(['Go', 'JS'])
const frontAndBackEnd = frontEndLanguages.intersection(backEndLanguages)
// => Set {"JS"}
元素 'JS'
是这两个集合中同时存在的唯一元素。
交集就像 INNER JOIN
。
Set.prototype.difference(other)
一个集合与另一个集合之间的差集是,第一个集合中存在、但另一个集合中不存在的所有元素。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const backEndLanguages = new Set(['Go', 'JS'])
const onlyFrontEnd = frontEndLanguages.difference(backEndLanguages)
// => Set {"HTML", "CSS"}
const onlyBackEnd = backEndLanguages.difference(frontEndLanguages)
// => Set {"Go"}
在查找集合之间的差集时,重要的是我们在哪个集合上调用函数,以及哪个集合是参数。在上述示例中,从前端语言中删除后端语言会导致 'JS'
被删除,并在结果集中返回 'HTML'
和 'CSS'
。然而,从后端语言中删除前端语言仍然会导致 'JS'
被删除,并返回 'Go'
。
差集就像执行 LEFT JOIN
。
Set.prototype.symmetricDifference(other)
两个集合之间的对称差集是一个集合,该集合包含存在于两个集合之一中的元素,但不包含两个集合中同时存在的元素。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const backEndLanguages = new Set(['Go', 'JS'])
const onlyFrontEnd = frontEndLanguages.symmetricDifference(backEndLanguages)
// => Set {"HTML", "CSS", "Go"}
const onlyBackEnd = backEndLanguages.symmetricDifference(frontEndLanguages)
// => Set {"Go", "HTML", "CSS"}
在这种场景下,结果集中的元素是相同的,但粉丝请注意,集合元素的顺序是不同的。集合顺序由元素添加到集合的顺序决定,并且执行该函数的集合会优先添加其元素。
对称差异类似于 FULL OUTER JOIN,排除两个表中的任何元素。
Set.prototype.isSubsetOf(other)
如果第一个集合中的所有元素都出现在第二个集合中,那么第一个集合就是第二个集合的子集。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const declarativeLanguages = new Set(['HTML', 'CSS'])
declarativeLanguages.isSubsetOf(frontEndLanguages) // true
frontEndLanguages.isSubsetOf(declarativeLanguages) // false
集合也是其自身的子集。
js
frontEndLanguages.isSubsetOf(frontEndLanguages) // true
Set.prototype.isSupersetOf(other)
如果第二个集合中的所有元素都出现在第一个集合中,那么第二个集合是第一个集合的超集。这是子集的相反关系。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const declarativeLanguages = new Set(['HTML', 'CSS'])
declarativeLanguages.isSupersetOf(frontEndLanguages) // false
frontEndLanguages.isSupersetOf(declarativeLanguages) // true
集合也是其自身的超集。
js
frontEndLanguages.isSupersetOf(frontEndLanguages) // true
Set.prototype.isDisjointFrom(other)
最后,如果一个集合与另一个集合没有共同元素,那么它们是不相交的。
js
const frontEndLanguages = new Set(['JS', 'HTML', 'CSS'])
const interpretedLanguages = new Set(['JS', 'Ruby', 'Python'])
const compiledLanguages = new Set(['Java', 'C++', 'TS'])
interpretedLanguages.isDisjointFrom(compiledLanguages) // true
frontEndLanguages.isDisjointFrom(interpretedLanguages) // false
这些集合中的解释型语言和编译型语言不重叠,因此这些集合是不相交的。前端语言和解释语言确实与 'JS'
元素重叠,因此它们并不是不相交的。
兼容性支持
目前,Set
新方法的相关提案处于 TC39 流程的第 3 阶段,苹果 Safari 17 去年 9 月已经发布,谷歌 Chrome 122 今年 2 月也已经实现了这些方法。微软 Edge 紧随 Chrome,Firefox Nightly 也有支持,估计这两种浏览器很快也会提供支持。
Bun 使用的是 Safari 的 JavaScriptCore 引擎,因此 Bun 已经支持这些新功能。Chrome 中的支持意味着它已被添加到 V8 JS 引擎中,且不久后会被 Node 采用。
这意味着,该提案大概率会进入流程的第 4 阶段,甚至可能在 ES2024 规范最终定稿之前及时加入。
Polyfills(功能补丁)
当我们需要较旧的 JS 引擎支持,我们可以使用 Polyfill 来升级到这些函数的规范实现。它们可以在 core-js
中使用,也可以作为 es-shims
项目中每个功能的单独包使用。举个栗子,Set.prototype.union
包可用于交集功能。
如果您已经编写了任何这些函数的自定义实现,我建议您首先升级到 polyfill,然后随着运行时支持变得更加广泛,再逐步淘汰它们。
Set
更加完备
JS 中的 Set
长期以来一直美中不足,但这 7 个全新函数很好地完善了 Set
的实现。将这样的功能构建到语言中意味着,我们可以减少对第三方模块或我们自己的实现的依赖,并且可以关注我们正在尝试解决的问题。
本期话题是 ------ 你最喜欢 Set
的哪个新方法?
欢迎在本文下方自由言论,文明共享。谢谢大家的点赞,掰掰~
《前端猫猫教》每日 9 点半更新,坚持阅读,自律打卡,每天一次,进步一点。