JS Set 新增原生交集并集:写过滤器代码量直接砍半,还不用引入 lodash

以前写前端,一碰到“从两个数组里找共同项”这种需求,我的第一反应是要么自己写循环加 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):返回对称差集,也就是“只在一个集合里出现”的元素。
  • 还有三个判断方法:isSubsetOfisSupersetOfisDisjointFrom

以前我们想要交集,最常见的姿势是这样的:

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) 甩给他,然后看他一脸“这是什么操作?”的表情。再扔给他一篇文档链接,功德圆满。

怎么样,今天这波热度蹭得还算实在?赶紧看看你的项目兼容性,启用它吧。

JS Set 新增原生交集并集:写过滤器代码量直接砍半,还不用引入 lodash
收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

版权声明:
本站资源有的来自互联网收集整理,本站纯免费分享提供学习使用,如果侵犯了您的合法权益,请联系本站我们会及时删除。
本站资源仅供研究、学习交流之用,免费开源项目不代表完全可商用,若商业用途请先咨询开发企业能否商用,否则产生的一切后果将由下载用户自行承担。
原创板块未经允许不得转载,否则将追究法律责任。

淘吗网 javascript JS Set 新增原生交集并集:写过滤器代码量直接砍半,还不用引入 lodash https://www.taomawang.com/web/javascript/2544.html

常见问题

相关文章

猜你喜欢
发表评论
暂无评论
官方客服团队

为您解决烦忧 - 24小时在线 专业服务