以前写前端,一碰到“从两个数组里找共同项”这种需求,我的第一反应是要么自己写循环加 includes,要么直接把 lodash/core 里的 _.intersection 拿过来。说实话,为了一个通用的交集方法引入一个库,心里总觉得有点小题大做。现在不用纠结了,因为 ECMAScript 提案里给 Set 加了 7 个新方法,浏览器已经陆续支持了。
我今天就用这些新方法写一个商品标签过滤器。过去这种逻辑,怎么也得写个十来行,现在几行就完事。而且这些方法读起来就像英文句子,同事看我代码再也不用挠头。
1. 这些新方法到底长啥样
Set 的新方法密密麻麻列出来一共 7 个,我不是来背文档的,只说几个常用的:
setA.union(setB):返回一个新 Set,包含 A 和 B 的所有元素,也就是并集。setA.intersection(setB):返回一个新 Set,包含既在 A 又在 B 的元素,也就是交集。setA.difference(setB):返回一个新 Set,包含在 A 里但不在 B 里的元素,也就是差集。setA.symmetricDifference(setB):返回对称差集,也就是“只在一个集合里出现”的元素。- 还有三个判断方法:
isSubsetOf、isSupersetOf、isDisjointFrom。
以前我们想要交集,最常见的姿势是这样的:
function intersect(a, b) {
return a.filter(x => b.includes(x))
}
这段代码问题不大,但语义不如 a.intersection(b) 直白。而且 includes 每次都要遍历数组,花的时间比 Set 的哈希查找高多了。现在一行:
const common = setA.intersection(setB)
这酸爽。
2. 实战:商品标签过滤器
光说不练假把式。我手头正好有一个商品列表,每个商品都有几个标签,比如“电子”“便携”“数码”这种。现在用户想根据选中的标签,找出同时包含这些标签的商品,就是一个交集过滤。
先看数据结构:
const products = [
{ id: 1, name: '手机', tags: ['电子', '数码', '便携'] },
{ id: 2, name: '笔记本', tags: ['电子', '电脑'] },
{ id: 3, name: 'T恤', tags: ['服装', '简单'] },
{ id: 4, name: '耳机', tags: ['电子', '数码', '便携'] }
]
页面上有几个复选框,值就是这些标签。当用户勾选“电子”和“数码”的时候,应该返回手机和耳机,因为它们都同时包含这两个标签。
以前我的写法可能是这样的:
function filterProducts(selectedTags) {
return products.filter(product => {
return selectedTags.every(tag => product.tags.includes(tag))
})
}
这样写没毛病。但如果我想用原生的 Set 新方法,可以这样写:
function filterProducts(selectedTags) {
const selectedSet = new Set(selectedTags)
return products.filter(product => {
const productSet = new Set(product.tags)
return selectedSet.isSubsetOf(productSet)
})
}
isSubsetOf 这个方法就是判断当前 Set 是不是另一个 Set 的子集,直接表达了“所有选中的标签都必须出现在商品的标签里”。如果你不习惯用 isSubsetOf,也可以用交集方式判断:
return selectedSet.intersection(new Set(product.tags)).size === selectedSet.size
两种写法都行,语义比 every includes 清晰不少。
3. 再做一个“并集”场景
有时候需求是“只要包含任何一个选中标签的商品”就算匹配。这时候就用上了 union 或者 isDisjointFrom。
比如我想找“和选中标签至少有一个重叠”的商品,用 isDisjointFrom 就很舒服:
function filterProductsByAny(selectedTags) {
const selectedSet = new Set(selectedTags)
return products.filter(product => {
const productSet = new Set(product.tags)
// 如果两个集合不是不相交的,说明有重叠
return !selectedSet.isDisjointFrom(productSet)
})
}
或者用 intersection 看一下交集大小是否大于0:
return productSet.intersection(selectedSet).size > 0
是不是很简单?这些方法让代码的意图直接长在方法名上,读起来跟伪代码差不多。
4. 完整的可运行示例
下面我把这个商品筛选器做成一个简单页面。因为只是演示,所以样式也不太讲究,主要看逻辑。你可以直接复制代码跑一下。
HTML 结构
<div id="app">
<h3>选择标签筛选商品</h3>
<div id="filters"></div>
<ul id="product-list"></ul>
</div>
JavaScript 逻辑
<script>
const products = [
{ id: 1, name: '手机', tags: ['电子', '数码', '便携'] },
{ id: 2, name: '笔记本', tags: ['电子', '电脑'] },
{ id: 3, name: 'T恤', tags: ['服装', '简单'] },
{ id: 4, name: '耳机', tags: ['电子', '数码', '便携'] }
]
// 收集所有标签
const allTags = [...new Set(products.flatMap(p => p.tags))]
// 渲染复选框
const filtersEl = document.getElementById('filters')
allTags.forEach(tag => {
const label = document.createElement('label')
const checkbox = document.createElement('input')
checkbox.type = 'checkbox'
checkbox.value = tag
label.append(checkbox, tag)
filtersEl.appendChild(label)
})
// 监听变化
const productListEl = document.getElementById('product-list')
filtersEl.addEventListener('change', render)
function render() {
const selected = [...document.querySelectorAll('input:checked')].map(cb => cb.value)
const result = selected.length === 0 ? products : products.filter(product => {
const selectedSet = new Set(selected)
const productSet = new Set(product.tags)
// 要求包含所有选中标签(交集等于选中集合)
return selectedSet.isSubsetOf(productSet)
})
productListEl.innerHTML = ''
result.forEach(p => {
const li = document.createElement('li')
li.textContent = p.name + ' —— ' + p.tags.join(' / ')
productListEl.appendChild(li)
})
}
// 初始渲染
render()
</script>
把这两个代码块放在一个 HTML 文件里,不需要任何第三方依赖,打开就是一面简单的筛选器。选中“电子”和“数码”,你只会看到手机和耳机,因为它们都同时有这两个标签。这就是 isSubsetOf 的功劳。
5. 那这个“新方法”到底新到哪去了?
Set 对象本身早就有了,只是缺少这些集合运算方法。这个提案早几年就有雏形,但直到 2024 年中,Chrome 在 122 版本里默认可用了。随后 Safari 17.3 和 Firefox 127 也跟上了,目前现代浏览器基本都支持了。
如果你还不放心,可以到 Can I use 上查一下“Set methods”这一条。如果项目还要兼容老旧的 WebView,可以在代码里加个 Polyfill。core-js 已经实现了这些方法,直接引入就行:
import 'core-js/modules/esnext.set.union.js'
import 'core-js/modules/esnext.set.intersection.js'
import 'core-js/modules/esnext.set.difference.js'
'... 按需引入'
或者你不想引入整个 core-js,也可以自己写一个简易版 polyfill。但说实话,2024 年都快过完了,项目还在跑远古浏览器的应该不多了。
6. 这些方法容易踩的坑
有几个细节我第一个想到就赶紧说:
- 这些方法都返回新的 Set,不会改变原 Set,用起来没有任何副作用。
- 它们都是基于 SameValueZero 规则判断元素相等的,也就是说
2和字符串"2"不会被看作同一个元素。 - 如果你在循环一个 Set 的过程中去修改它,还是老规矩,别这么干。跟这些新方法无关。
isSubsetOf这类方法,不需要两个 Set 类型完全一致,它会按元素值比较。
还有一个小建议:如果后端返回的是数组,你对数组调用这些方法前,记得先 new Set() 包一下,不然会报错。
7. 用这些方法还能做啥
除了商品过滤,我当时还发现一个很好的场景:做权限判断。比如用户拥有权限列表,某页面需要同时拥有两个权限才能访问。这时候你只要把用户权限 Set 和需求权限 Set 做交集,然后判断交集是否等于需求权限 Set,一行代码就搞定。
const canAccess = userPermissions.intersection(requiredPermissions).size === requiredPermissions.size
再比如做多选数据同步、标签系统、购物车优惠券重叠判断,反正凡是集合运算,现在都舒服多了。特别是并集,以前要一行一行地 push 再剔重,现在 union 直接给你结果。
8. 用着用着,我再也不想用 lodash 的集合函数了
倒不是说 lodash 不好,只是对于纯前端的简单数据处理,原生 API 已经够用。减少一个依赖,意味着打包体积更小,加载更快。而且原生的这些方法在名字上更贴近语言习惯,代码读起来更清爽。
如果你现在的项目环境支持这些新方法,我建议你把那些不必要的 _.intersection、_.union 都换掉,既减少包体积,又能提升一点点性能(尤其是大数据量时,哈希查找比 includes 的线性查找快不少)。
不过要提醒一下,如果你使用了 Babel 或 TypeScript,并且编译目标很老,记得配置 polyfill 或者直接用 core-js。别兴致勃勃写完,到线上暴露出兼容问题。
9. 最后来个总结
Set 的新方法本质上就是把数学里的集合运算搬进了 JavaScript。它们不是什么黑魔法,只是一个早就该死等人实现的 API。作为前端,我们不需要去背每个方法的定义,只要知道遇到交集、并集、差集这类操作时,顺手用一下就行。
以后要是有人问“没有 lodash 怎么求两个数组的交集”,你可以直接把 setA.intersection(setB) 甩给他,然后看他一脸“这是什么操作?”的表情。再扔给他一篇文档链接,功德圆满。
怎么样,今天这波热度蹭得还算实在?赶紧看看你的项目兼容性,启用它吧。

