过去做前端面试题,十次有八次会碰到“合并数组去重”或者“获取两个数组中共同元素”。以前我们写出来的代码总是又长又绕,要么用 filter 加 includes,要么自己维护一个哈希表。等终于交完卷,心里还七上八下,总怕漏了边界情况。
今年 ECMAScript 2025 正式把 Set 的新方法推到了所有主流浏览器里。也就是说,你现在打开 Chrome 126 或者 Node 22,直接就能用 set.union()、set.intersection()、set.difference() 这些原生的集合运算方法,再也不需要手写算法。
这篇文章我就拿一个实际的项目需求来跑一遍,把常用集合操作方法全部串起来。看完你会发现,以前绕来绕去的逻辑,现在只要一行代码。
一、新的 Set 方法都有哪些
这次新增的方法一共七个,先列个清单:
union(其他Set):返回两个集合的并集 intersection(其他Set):返回两个集合的交集 difference(其他Set):返回在当前集合里但不在其他集合里的元素 symmetricDifference(其他Set):返回对称差集(两个集合中只属于其中一个的元素) isSubsetOf(其他Set):判断当前集合是否是另一个集合的子集 isSupersetOf(其他Set):判断当前集合是否是另一个集合的超集 isDisjointFrom(其他Set):判断两个集合是否有交集(完全不相交)
这里有个重要细节:union、intersection、difference、symmetricDifference 都返回一个新的 Set 对象,不会修改原来的 Set。而 isSubsetOf 这种返回的是布尔值。
还有一个容易踩的坑:这些方法接受的不一定是 Set,也可以是具有 length 属性的类数组对象。比如 NodeList、arguments、数组都可以直接传进去。底层会按可迭代对象处理。
二、一个真实的场景:权限标签合并
我先说下我最近碰到的一个需求:后台管理系统里有两种角色,每个角色都有一串权限标签。比如“编辑”角色拥有 read, write, download, share 这四种权限;而“审核”角色拥有 read, download, approve 这三种权限。
现在要做三个功能:
- 合并两个角色的权限,生成一个“最大权限集合”,用于给这个用户生成菜单。
- 找出两个角色共同拥有的权限,用于决定是否显示某些共用按钮。
- 找出“编辑”角色独有而“审核”角色没有的权限,用于给编辑角色额外的操作提示。
以前我可能会写这样一串代码:
const editor = new Set(['read', 'write', 'download', 'share']); const reviewer = new Set(['read', 'download', 'approve']); // 并集(老写法) const union = new Set([...editor, ...reviewer]); // 交集(老写法) const intersection = new Set([...editor].filter(item => reviewer.has(item))); // 差集(editor有而reviewer没有) const difference = new Set([...editor].filter(item => !reviewer.has(item)));
这段代码没问题,但每次都要展开数组、调用 filter,逻辑稍微复杂一点就容易把方向搞反。尤其是差集,有时候我写着写着就分不清到底是谁减谁了。
现在用新方法,代码直接变成:
const editor = new Set(['read', 'write', 'download', 'share']);
const reviewer = new Set(['read', 'download', 'approve']);
// 并集
const union = editor.union(reviewer);
// 结果:Set {'read', 'write', 'download', 'share', 'approve'}
// 交集
const intersection = editor.intersection(reviewer);
// 结果:Set {'read', 'download'}
// 差集(editor 有而 reviewer 没有)
const difference = editor.difference(reviewer);
// 结果:Set {'write', 'share'}
注意最后那个差集是 editor.difference(reviewer),意思是“editor 减去 reviewer 的部分”。另外还有一个 reviewer.difference(editor),返回 {'approve'}。方向非常明确,看方法名就能懂。
我还顺手验证了一下对称差集 symmetricDifference,它返回的是两个集合中除了交集之外的所有元素,也就是“你我有我但并集减去交集”:
const symDiff = editor.symmetricDifference(reviewer);
// 结果:Set {'write', 'share', 'approve'}
这个功能在“找两个用户之间权限差异”的提示里非常有用,省得自己手动 `union – intersection`。
三、自动去重的数组操作
除了集合之间运算,这几个方法也可以直接用来处理数组。因为数组本身是可迭代对象,可以直接传进去,没有必要先转 Set 再传。
举个例子,我有两个数组:一个是在校学生的ID列表,另一个是体育兴趣小组的报名ID列表。我想知道哪些学生既报名了兴趣小组,又是在校学生(查重合)。
const allStudents = [101, 102, 103, 104, 105]; const clubMembers = [102, 104, 106]; // 直接用数组调用 intersection,注意调用者必须是 Set,参数可以是数组 const common = new Set(allStudents).intersection(clubMembers); console.log([...common]); // [102, 104]
记住调用者必须是 Set。如果你写成 allStudents.intersection(clubMembers) 会直接报错,因为数组没有这个方法。但参数那里随便,数组、Set、生成器都能用。
所以如果你不想改原有数组类型,就先用 new Set(allStudents) 包一层,然后把另一个数组直接塞进去。
四、判断包含关系,逻辑清晰不烧脑
权限系统里经常要判断:某个用户是否拥有所有“管理员”权限?如果你有一组用户权限,一组必要权限,可以用 isSupersetOf:
const userPerms = new Set(['read', 'write', 'delete', 'export']);
const requiredPerms = new Set(['read', 'write']);
if (userPerms.isSupersetOf(requiredPerms)) {
console.log('用户满足所有必需权限');
} else {
console.log('用户缺少部分权限');
}
反过来,如果你想确认两个分组之间没有任何交集,比如“线上环境”和“测试环境”的发布机器列表不能重叠,可以用 isDisjointFrom:
const onlineHosts = new Set(['192.168.1.1', '192.168.1.2']);
const testHosts = new Set(['10.0.0.5', '10.0.0.8', '192.168.1.2']);
if (onlineHosts.isDisjointFrom(testHosts)) {
console.log('环境完全隔离');
} else {
console.log('有机器被复用,需要检查');
}
这里输出“有机器被复用”,因为两个集合都包含 192.168.1.2。用这个函数做环境隔离的校验,比手写循环清爽得多。
还有 isSubsetOf,判断一个集合是否完全被另一个集合包含。比如检查“所选标签列表”是否都是“用户已订阅标签”的子集,防止选择未订阅的标签。
五、为什么你不必再写工具函数了
以前 lodash 里也有 _.union、_.intersection,但那是按数组处理的。现在 Set 原生方法更符合集合语义,而且返回值是 Set,可以直接继续链式调用。
举个链式调用的例子:我想知道“编辑”和“审核”共有的权限,并进一步判断这些共有权限是否都是“超级管理员”的子集。
const admin = new Set(['read', 'write', 'download', 'approve', 'share']);
const result = editor
.intersection(reviewer)
.isSubsetOf(admin);
console.log(result); // true
一气呵成,不需要定义中间变量。这种表达式式的写法,在复杂业务里可以少声明很多无意义的变量。
另一个实用技巧:配合展开运算符转回数组。比如合并数组去重,以前你写:
const merged = [...new Set([...arr1, ...arr2])];
现在可以这样:
const merged = [...new Set(arr1).union(new Set(arr2))];
但说实话,第一种已经够简洁了。所以新方法真正的价值不在这种简单合并上,而是在需要语义化判断和多个集合运算链式处理的时候。
六、浏览器兼容性,以及怎么用
目前所有现代浏览器都支持了:Chrome 126+,Edge 126+,Firefox 127+,Safari 17+。Node.js 22.0.0 自带这些方法,不用任何 flag。如果你还在维护老项目,可以装一个 polyfill:core-js 里已经包含 Set 的所有新增方法。
安装方式就是:
npm install core-js
然后在入口文件里引入:
import 'core-js/features/set/union'; import 'core-js/features/set/intersection'; import 'core-js/features/set/difference'; import 'core-js/features/set/symmetric-difference'; import 'core-js/features/set/is-subset-of'; import 'core-js/features/set/is-superset-of'; import 'core-js/features/set/is-disjoint-from';
如果你用的是 TypeScript,需要确保 lib 配置包含 "ES2025" 或者直接 "ESNext"。目前 @typescript/lib-esnext 已经把这些方法定义好了。
七、踩坑提醒:NaN 和对象的处理
Set 的比较用的是 SameValueZero 算法,所以 NaN 可以正常被 Set 处理。比如:
const a = new Set([NaN, 1]);
const b = new Set([NaN, 2]);
console.log(a.intersection(b)); // Set {NaN}
这倒是挺好,但在实际业务中很少遇到。关键要注意的是对象,因为对象比较的是引用。如果你往 Set 里添加了两个结构完全相同但不是同一个引用的对象,它们会被视为不同的元素。所以在做交集、差集时,如果处理的是对象数组,最好先映射成 ID 数组再做运算,否则结果可能不符合预期。
另外一个坑:这些 Set 方法虽然返回 Set,但它们不会对参数做类型转换。如果传入一个字符串,因为字符串也是可迭代的,它会按字符拆成单元素集合。你想传一个字符串值像 "管理员" 作为单个元素?对不起,会被拆成一个个汉字。所以如果你要传字符串,要先包在数组里 new Set(["管理员"])。
八、实际案例:实现一个简单的标签筛选器
最后写一个稍微完整点的案例。假设一个博客系统里,文章标签有“前端”、“JavaScript”、“CSS”、“后端”、“Node.js”、“工程化”。用户可以选择多个偏好标签,然后系统需要根据偏好过滤文章。
每篇文章存储时带一个标签数组,我们需要找出“包含用户所有偏好标签的文章”。实际上,就是判断文章的标签集合是否是用户偏好标签的超集。
const articles = [
{ id: 1, title: 'JS 异步编程', tags: ['JavaScript', '前端'] },
{ id: 2, title: 'CSS 容器查询', tags: ['CSS', '前端'] },
{ id: 3, title: 'Node.js 后端实践', tags: ['Node.js', '后端'] },
{ id: 4, title: '前端工程化', tags: ['JavaScript', '前端', '工具'] },
{ id: 5, title: 'Java 还是 Node?', tags: ['后端', 'Node.js'] },
];
const userPrefs = new Set(['前端', 'JavaScript']);
const perfectMatches = articles.filter(article => (
new Set(article.tags).isSupersetOf(userPrefs)
));
console.log(perfectMatches);
// 只输出 id=1 和 id=4 的文章
如果需求改成“找出包含任意一个用户偏好标签的文章”,那就用 intersection 判断交集是否为空:
const anyMatch = articles.filter(article => (
new Set(article.tags).intersection(userPrefs).size > 0
));
看到没,以前要写两个循环加 includes,现在直接 Set 运算一行搞定。
九、总结
这些 Set 新增方法让我最开心的不是省了那几行字,而是语义上更准确。difference 一眼就知道是差集,isDisjointFrom 一眼就知道是不相交。人脑不需要再脑补 filter 和 has 的组合逻辑,代码本身就表达了意图。
在业务中,只要你遇到“去重”、“共同拥有”、“只在一个集合里”这类需求,都可以优先考虑直接用 Set 新方法。即便你还在用老环境,核心-js polyfill 也就几行引用的事。
JavaScript 的进步真的在慢慢补齐以前各种不方便的地方。从 Array.prototype.at 到 structuredClone,再到现在的 Set 方法,标准库越来越像我们期望中的样子。这种演进让我觉得,有时候不需要什么高深技巧,标准库多一个贴心的 API,就是最大的生产力提升。

