JavaScript Set基础用法:创建、增删改查与遍历代码实例
深入解析JavaScript ES6 Set集合数据结构的基础用法:从创建初始化、增删改查(CRUD)操作,到for...of与迭代器遍历,结合数组去重与集合运算实战代码实例与避坑指南。
includes() 或 indexOf() 手动查重,不仅逻辑琐碎,其线性扫描在数据量增长时还会带来 $O(N)$ 的性能损耗。ES6 引入的原生 Set 数据结构提供了天然的唯一性保证与 $O(1)$ 的高速检索能力。本文将从创建初始化、增删改查(CRUD)到遍历与集合运算,通过具体代码与执行示例完整讲解 Set 的实用用法。
一、认识 Set:核心特性与底层判定机制
Set 是 JavaScript ES6(ECMAScript 2015)正式推出的内置集合对象。它类似于数组,但具有两个最根本的差异:
- 元素唯一性:集合内部不允许存在两个完全相同的值,任何重复添加操作都会被自动且静默地忽略。
- 保持插入时序:尽管 Set 基于哈希结构实现高效检索,但它在规范层面保证了严格按照元素的插入顺序进行迭代。
很多开发者容易混淆 Set 判定“两值是否重复”的规则。Set 内部采用的是 ECMAScript 规范中的 SameValueZero 算法。该算法与全等比对运算符 === 非常接近,主要有两点关键区别:
- NaN 的处理:在
===运算中,NaN === NaN为false;而在 Set 内部,NaN被视为相互相等,多次添加NaN只会保留一份。 - 符号零的处理:正零
+0与负零-0在 Set 中被判定为完全相同,不会重复存储。
{ id: 1 }),它们在堆内存中的地址不同,Set 会将它们作为两个独立元素保存。
二、创建 Set 实例(初始化)
创建 Set 实例统一使用 new Set() 构造函数。你可以创建一个初始为空的集合,也可以传入任何遵循可迭代协议(Iterable)的数据结构(如 Array、String、NodeList 等)完成快速填充与去重。
1. 创建空集合
const emptySet = new Set();
console.log(emptySet.size); // 示例输出: 0
2. 使用数组或其他可迭代对象初始化
当传入包含重复项的数组时,构造函数会在初始构建时一次性完成去重:
// 传入带有重复项的数组
const numberSet = new Set([10, 20, 20, 30, 10]);
console.log(numberSet); // 示例输出: Set(3) { 10, 20, 30 }
console.log(numberSet.size); // 示例输出: 3
// 传入字符串(字符串本身也是可迭代对象,自动拆解字符并去重)
const charSet = new Set('javascript');
console.log(charSet); // 示例输出: Set(9) { 'j', 'a', 'v', 's', 'c', 'r', 'i', 'p', 't' }
arr.length 属性,而读取 Set 中元素数量必须使用 set.size 属性。Set 没有 length 属性,直接访问会返回 undefined。
三、Set 的增删改查(CRUD)操作
Set 提供了一组语义极其清晰的原生操作 API。需要特别理解的是:由于 Set 是“无键、无索引”的值集合,其增删查与常规数组存在本质差异。
1. 增(Create / Add):add() 方法
调用 set.add(value) 向集合末尾追加一个新元素。如果该元素已存在,则不会发生任何改动。该方法返回当前 Set 实例本身,因此支持连贯的链式调用。
const skills = new Set();
// 基础单次添加
skills.add('HTML');
skills.add('CSS');
// 链式调用
skills.add('JavaScript').add('TypeScript');
// 重复添加同一元素,不会产生冗余
skills.add('HTML');
console.log(skills.size); // 示例输出: 4
console.log(skills); // 示例输出: Set(4) { 'HTML', 'CSS', 'JavaScript', 'TypeScript' }
2. 删(Delete):delete() 与 clear() 方法
Set 提供了单项删除与一键清空两种删除操作:
set.delete(value):从集合中移除指定元素。若该元素原本存在且删除成功,返回true;若元素不存在,返回false。set.clear():清除集合中的所有成员,无返回值(undefined),调用后size变为 0。
const statusCodes = new Set([200, 404, 500]);
// 成功删除存在的值
const result1 = statusCodes.delete(404);
console.log(result1); // 示例输出: true
console.log(statusCodes.size); // 示例输出: 2
// 尝试删除不存在的值
const result2 = statusCodes.delete(999);
console.log(result2); // 示例输出: false
// 一键清空整个集合
statusCodes.clear();
console.log(statusCodes.size); // 示例输出: 0
3. 查(Read / Query):has() 方法
set.has(value) 用于检测集合中是否包含某个特定元素,返回布尔值(true 或 false)。
这是 Set 相对于 Array 的核心性能优势所在:数组的 arr.includes(val) 必须从头到尾进行线性对比,时间复杂度为 $O(N)$;而 Set 的底层基于哈希表寻址,set.has(val) 的平均时间复杂度仅为 $O(1)$,在大数据量判断时速度提升尤为明显。
const rolePermissions = new Set(['user:read', 'user:write', 'order:pay']);
console.log(rolePermissions.has('user:read')); // 示例输出: true
console.log(rolePermissions.has('admin:manage')); // 示例输出: false
// 注意:Set 无法通过数值下标随机访问元素
console.log(rolePermissions[0]); // 示例输出: undefined
4. 改(Update):更新与替换策略
由于 Set 不具备类似数组的索引(如 arr[0] = newVal)或对象的键映射(如 map.set(key, val)),因此原生并没有提供名为 update() 的 API。修改现有元素通常分为两种业务场景:
-
场景 A:修改基础原始类型(替换值)
推荐模式是“先delete旧值,再add新值”。这样既能保证原值彻底被注销,又能让新值通过 SameValueZero 检查。 -
场景 B:修改引用对象内的属性
若 Set 中存放的是引用对象,直接修改对象内部的属性并不会改变对象本身的内存指针引用,该对象在 Set 中的存储依然有效。
// 场景 A:替换原始值(例如重命名分类)
const categories = new Set(['Tech', 'Life', 'Finance']);
function replaceSetValue(targetSet, oldValue, newValue) {
if (targetSet.has(oldValue)) {
targetSet.delete(oldValue);
targetSet.add(newValue);
return true;
}
return false;
}
replaceSetValue(categories, 'Tech', 'Science & Tech');
console.log(categories); // 示例输出: Set(3) { 'Life', 'Finance', 'Science & Tech' }
// 场景 B:修改引用对象的属性
const userSession = { id: 101, active: false };
const activeUsers = new Set([userSession]);
// 直接修改对象内部属性,Set 保持对其引用的跟踪
userSession.active = true;
console.log([...activeUsers][0].active); // 示例输出: true
四、Set 的遍历与迭代实战
Set 原生实现了可迭代协议(Iterable),因此能够与现代 JavaScript 的各种迭代语法无缝协作。所有遍历方式都严格按照元素的插入时序逐个产出。
1. for...of 循环(首选语法)
for...of 是遍历 Set 最直观、最符合现代工程规范的写法。它支持在循环体内使用 break、continue 或 return 提早中断控制流。
const frameworks = new Set(['Vue', 'React', 'Svelte']);
for (const framework of frameworks) {
console.log('框架名称:', framework);
}
/* 示例输出:
框架名称: Vue
框架名称: React
框架名称: Svelte
*/
2. forEach() 回调遍历
Set 也提供了与 Array 类似的 forEach((value, key, set) => ...) 方法。
Map.prototype.forEach 的参数签名保持完全统一,规定回调函数中的前两个参数 value 和 key 接收相同的值。
const tags = new Set(['frontend', 'backend']);
tags.forEach((value, key, ownerSet) => {
console.log(`value: ${value}, key: ${key}, 恒等: ${value === key}`);
});
/* 示例输出:
value: frontend, key: frontend, 恒等: true
value: backend, key: backend, 恒定: true
*/
3. keys()、values() 与 entries() 迭代器
Set 实现了这三个返回迭代器(Iterator)的标准方法:
set.values():返回包含集合所有元素的迭代器。这是 Set 的默认迭代器(即set[Symbol.iterator]的等效方法)。set.keys():为了与 Map 接口对齐而提供,其行为与set.values()完全一致。set.entries():返回键值对迭代器,产出形式为[value, value]的二元数组。
const colors = new Set(['red', 'blue']);
for (const [key, val] of colors.entries()) {
console.log(`[${key}, ${val}]`);
}
/* 示例输出:
[red, red]
[blue, blue]
*/
4. 展开语法(...)与 Array.from() 转回数组
由于 Set 本身没有提供 map()、filter() 或 reduce() 等数组高阶操作,我们在实际业务中经常使用展开运算符(Spread Syntax)或 Array.from() 快速将 Set 转回 Array 进行复杂加工:
const scores = new Set([60, 85, 95, 60]);
// 展开为数组并使用 map 处理
const doubled = [...scores].map(n => n * 2);
console.log(doubled); // 示例输出: [ 120, 170, 190 ]
五、实战应用:数组去重与经典集合运算
在日常开发中,利用 Set 实现集合运算不仅代码简明,执行效率也显著优于多次嵌套循环。
1. 经典一行代码实现数组去重
const rawUserIds = [1001, 1002, 1001, 1003, 1002];
// 单行完成基础数据类型去重
const uniqueIds = [...new Set(rawUserIds)];
console.log(uniqueIds); // 示例输出: [ 1001, 1002, 1003 ]
2. 并集、交集与差集的实现
假如有集合 A 和集合 B,我们可以通过结合扩展运算符和 set.has() 快速完成数学集合运算:
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// 并集 (Union):合并两集合的所有不重复成员
const union = new Set([...setA, ...setB]);
console.log([...union]); // 示例输出: [ 1, 2, 3, 4, 5, 6 ]
// 交集 (Intersection):仅保留同时存在于 A 和 B 的成员
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log([...intersection]); // 示例输出: [ 3, 4 ]
// 差集 (Difference):保留存在于 A 但不存在于 B 的成员 (A \ B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log([...difference]); // 示例输出: [ 1, 2 ]
Set.prototype 新增了原生集合运算方法,包括 setA.union(setB)、setA.intersection(setB) 及 setA.difference(setB) 等。在现代浏览器或新版 Node.js 中可直接使用原生方法,而上述基于 filter 和 has 的写法依然是全平台通用的最佳兼容方案。
六、常见开发误区与避坑指南
| 误区场景 | 典型错误代码 | 正确应对方式 |
|---|---|---|
| 对象字面量无法自动去重 | new Set([{ id: 1 }, { id: 1 }]).size === 2 |
对象按内存地址比对,字面量引用不同。对象数组去重建议先序列化键(如 Map 按主键 ID 去重)。 |
| 误用 length 属性获取大小 | const count = mySet.length; // undefined |
必须使用 mySet.size 获取集合元素数量。 |
| 直接调用数组专属方法 | mySet.map(x => x * 2); // TypeError: not a function |
先使用展开语法转为数组 [...mySet].map(...),或使用 for...of 循环迭代。 |
| 试图通过数字下标取值 | const first = mySet[0]; // undefined |
使用解构 const [first] = mySet; 或 mySet.values().next().value。 |
综上所述,JavaScript Set 不仅是实现高效数组去重的利器,更是处理无序排他元素、权限校验以及复杂集合运算的核心底层数据结构。熟练掌握其增删查 API 与遍历方式,能显著提高代码的执行效率与逻辑清晰度。
JavaScript怎么准确判断数据类型:typeof、instanceof与Object.prototype.toString
2026-11-05 17:09:21
JS中将数组和对象混合组装为JSON字符串
2026-11-05 16:41:42
JS运算符优先级与结合性的区别是什么?
2026-11-05 13:22:02
JS Map怎么只遍历Key或Value
2026-11-05 13:00:36
D3.js 与 ECharts 选型对比:灵活性 vs 开发效率
2026-11-05 09:45:01






![JS创建二维数组:为什么Array(n).fill([])会导致数据联动](https://img.javascript.cn/tit/articles/13236/20261102/f618c991891ff37efb12df6af2ce0ccb0da8779a907fe1d2f7275d60c84fdd77.webp)

