Set 和 Map 都是 ES6 新增的有序集合类型 (区别于 Object 的无序/类型限制),但设计目标完全不同:Set 是「无重复值的集合」(仅存值,无键),核心用于去重、存在性判断 ;Map 是「键值对集合」(键值映射),核心用于灵活的键值存储(替代 Object 解决键类型限制问题)。以下是全方位对比和实战指南。
一、核心定位(最本质区别)
| 集合类型 | 核心结构 | 核心特性 | 形象比喻 |
|---|---|---|---|
| Set | 「值的集合」 | 存储唯一、无序的值(无键,仅值),自动去重 | 像"无重复的数组",只关心值是否存在 |
| Map | 「键值对集合」 | 存储键值对(键任意类型、有序、唯一),无原型干扰 | 像"升级版 Object",键可存对象/数组 |
二、关键特性对比表
| 特性维度 | Set(值集合) | Map(键值对集合) |
|---|---|---|
| 存储单元 | 仅存储「值」(value),无键 | 存储「键-值」对(key-value),键值一一映射 |
| 唯一性规则 | 「值」唯一(重复值添加无效),NaN 视为相等 |
「键」唯一(重复键会覆盖值),键判断规则同 Set |
| 键/值类型限制 | 无键,值可以是任意类型(原始类型/引用类型) | 键可以是任意类型(对象/数组/函数等),无类型转换;值也可任意类型 |
| 顺序 | 严格遵循「插入顺序」存储/迭代 | 严格遵循「插入顺序」存储/迭代 |
| 获取大小 | 内置 size 属性(直接返回值的数量) |
内置 size 属性(直接返回键值对数量) |
| 迭代方式 | 可直接 for...of/forEach,迭代值本身 |
可直接 for...of/forEach,迭代 [key, value] 数组 |
| 常用方法 | add()/delete()/has()/clear()/values() |
set()/get()/delete()/has()/clear()/keys()/values()/entries() |
| 是否可索引访问 | 无索引(不能用 set[0] 获取值) |
无索引(不能用 map[0] 获取值,需用 get(key)) |
| JSON 序列化 | 默认 JSON.stringify(set) 得到 [],需手动转换 |
默认 JSON.stringify(map) 得到 {},需手动转换 |
三、基础用法示例
1. Set 核心用法(去重、存在性判断)
javascript
// 1. 创建 Set(可传入数组初始化)
const s = new Set([1, 2, 2, 3]); // 自动去重,s = Set(3) {1, 2, 3}
console.log(s.size); // 3
// 2. 新增值(重复值添加无效)
s.add(4); // Set(4) {1,2,3,4}
s.add(2); // 无变化(值已存在)
// 3. 判断值是否存在
console.log(s.has(2)); // true
console.log(s.has(5)); // false
// 4. 删除值
s.delete(3); // true(删除成功)
console.log(s); // Set(3) {1,2,4}
// 5. 迭代(三种方式)
// 方式1:for...of
for (const val of s) {
console.log(val); // 1 → 2 → 4(插入顺序)
}
// 方式2:forEach
s.forEach(val => console.log(val));
// 方式3:转为数组(常用!利用 Set 去重)
const arr = [...s]; // [1,2,4]
const uniqueArr = [...new Set([1,2,2,3])]; // [1,2,3]
// 6. 清空
s.clear();
console.log(s.size); // 0
2. Map 核心用法(灵活键值存储)
javascript
// 1. 创建 Map(可传入二维数组初始化)
const m = new Map([
['name', '张三'],
[123, '数字键'],
[{ id: 1 }, '对象键'] // 键可以是对象
]);
console.log(m.size); // 3
// 2. 新增/修改键值对(重复键覆盖值)
m.set('age', 18); // Map(4) {..., 'age' => 18}
m.set(123, '覆盖数字键'); // 键123的值被覆盖
// 3. 获取值(必须用 get(key),不能用 m.key)
console.log(m.get('name')); // 张三
console.log(m.get({ id: 1 })); // undefined(对象键是引用类型,需用同一个对象)
const objKey = { id: 1 };
m.set(objKey, '正确的对象键');
console.log(m.get(objKey)); // 正确的对象键
// 4. 判断键是否存在
console.log(m.has('age')); // true
// 5. 删除键值对
m.delete('age'); // true
// 6. 迭代(三种方式)
// 方式1:for...of(迭代 [key, value])
for (const [key, value] of m) {
console.log(key, value); // name 张三 → 123 覆盖数字键 → {id:1} 正确的对象键
}
// 方式2:forEach(value 在前,key 在后)
m.forEach((value, key) => console.log(key, value));
// 方式3:迭代键/值
for (const key of m.keys()) console.log(key); // 所有键
for (const value of m.values()) console.log(value); // 所有值
// 7. 清空
m.clear();
console.log(m.size); // 0
四、核心差异实战示例
示例 1:Set 去重(最常用场景)
javascript
// 数组去重(含 NaN,Set 视 NaN 为相等)
const arr = [1, 2, 2, NaN, NaN, { id: 1 }, { id: 1 }];
const uniqueArr = [...new Set(arr)];
console.log(uniqueArr); // [1,2,NaN, {id:1}, {id:1}](对象是引用类型,视为不同值)
// 字符串去重
const str = 'aabbcc';
const uniqueStr = [...new Set(str)].join(''); // 'abc'
示例 2:Map 解决 Object 键类型限制
javascript
// Object 无法用对象做键(会转为字符串 "[object Object]")
const obj = {};
const key1 = { id: 1 };
const key2 = { id: 2 };
obj[key1] = '值1';
obj[key2] = '值2';
console.log(obj[key1]); // 值2(因为 key1/key2 都转为 "[object Object]",覆盖了)
// Map 可以用对象做键(引用类型键精准匹配)
const map = new Map();
map.set(key1, '值1');
map.set(key2, '值2');
console.log(map.get(key1)); // 值1(精准匹配)
console.log(map.get(key2)); // 值2
示例 3:Set 实现交集/并集/差集
javascript
const s1 = new Set([1, 2, 3]);
const s2 = new Set([3, 4, 5]);
// 并集
const union = new Set([...s1, ...s2]); // Set(5) {1,2,3,4,5}
// 交集
const intersection = new Set([...s1].filter(x => s2.has(x))); // Set(1) {3}
// 差集(s1 有但 s2 没有)
const difference = new Set([...s1].filter(x => !s2.has(x))); // Set(2) {1,2}
五、适用场景选型(一句话记住)
| 场景需求 | 选 Set | 选 Map |
|---|---|---|
| 数组/字符串去重、判断值是否存在 | ✅ | ❌ |
| 存储唯一值集合(如标签、权限列表) | ✅ | ❌ |
| 实现交集/并集/差集等集合运算 | ✅ | ❌ |
| 键值对存储(尤其是键为非字符串类型) | ❌ | ✅ |
| 动态增删键值对、依赖插入顺序迭代 | ❌ | ✅ |
| 替代 Object 避免原型链冲突 | ❌ | ✅ |
典型实战场景
-
Set 用例:
- 标签管理(如文章标签去重);
- 权限校验(判断用户是否有某个权限);
- 数组去重、集合运算。
-
Map 用例:
- 缓存存储(如用 DOM 元素作为键存储对应数据);
- 复杂键值对存储(如键为函数/数组);
- 替代 Object 存储动态键值(避免
toString/hasOwnProperty等原型键冲突)。
六、常见坑点与避坑
1. Set/Map 的引用类型判断
Set 的值、Map 的键若为引用类型(对象/数组),判断"相等"的依据是引用地址,而非内容:
javascript
const s = new Set();
s.add({ id: 1 });
s.add({ id: 1 });
console.log(s.size); // 2(两个对象引用地址不同,视为不同值)
const obj = { id: 1 };
s.add(obj);
s.add(obj);
console.log(s.size); // 3(重复添加同一个对象,仅存一个)
2. NaN 的特殊性
Set/Map 视 NaN 为"相等"(即使 NaN !== NaN):
javascript
const s = new Set([NaN, NaN]);
console.log(s.size); // 1(自动去重)
const m = new Map();
m.set(NaN, '值1');
m.set(NaN, '值2');
console.log(m.get(NaN)); // 值2(重复键覆盖)
3. 无法通过索引访问
Set/Map 都没有索引(如 set[0]/map[0]),需通过迭代或 get 方法获取值:
javascript
const s = new Set([1,2,3]);
console.log(s[0]); // undefined(错误用法)
console.log([...s][0]); // 1(正确:转为数组后访问)
const m = new Map([['name', '张三']]);
console.log(m['name']); // undefined(错误用法)
console.log(m.get('name')); // 张三(正确:用 get 方法)
4. JSON 序列化问题
Set/Map 默认无法被 JSON.stringify 正确序列化,需手动转换:
javascript
// Set 转为数组序列化
const s = new Set([1,2,3]);
const sJson = JSON.stringify([...s]); // "[1,2,3]"
// Map 转为二维数组序列化
const m = new Map([['name', '张三'], ['age', 18]]);
const mJson = JSON.stringify([...m]); // "[["name","张三"],["age",18]]"
// 反序列化
const mNew = new Map(JSON.parse(mJson)); // 恢复为 Map
七、总结
| 核心结论 | Set | Map |
|---|---|---|
| 核心功能 | 去重、存唯一值、集合运算 | 灵活键值存储(替代 Object) |
| 核心优势 | 自动去重、简洁的存在性判断 | 键任意类型、有序、无原型冲突 |
| 实战关键词 | 去重、标签、权限、集合运算 | 缓存、复杂键、动态键值对 |
简单来说:「存唯一值用 Set,存键值对用 Map」 。两者都是 ES6 为解决传统数组/对象缺陷设计的集合类型,Set 专注"值的唯一性",Map 专注"灵活的键值映射",配合数组的扩展运算符(...)可轻松实现转换和运算。