Set 和 Map:去重、映射与高效查找
当你遇到下面这些需求时,Set 和 Map 往往比 Array、Object 更合适:
- 数组去重
- 快速判断某个值是否存在
- 统计词频 / 计数
- 用对象、函数、DOM 节点当键
- 需要“按插入顺序遍历”的键值结构
很多人知道它们“能用”,但不知道为什么该用、和 Array / Object 到底差在哪、有哪些坑。
这篇文档的目标是:用尽量短的时间,把 Set、Map、WeakSet、WeakMap 的核心知识一次讲清楚。
1. 先建立整体认知:Set 和 Map 分别解决什么问题?
1.1 Set:一组“唯一值”
Set 本质上是一个值的集合,特点是:同一个值只能出现一次。
const set = new Set([1, 2, 2, 3]);
console.log(set); // Set(3) {1, 2, 3}
console.log(set.size); // 3
典型用途:
- 数组去重
- “是否访问过”标记
- 白名单 / 黑名单判断
- 求并集、交集、差集
1.2 Map:更强的键值对结构
Map 本质上是一个键值对集合,但它比 Object 更灵活:
- 键可以是任意类型,不只限于字符串
- 保留插入顺序
- 天然适合频次统计、缓存、索引表
const map = new Map();
const user = {id: 1};
map.set(user, '管理员');
console.log(map.get(user)); // 管理员
1.3 一张表看懂差异
| 结构 | 存什么 | 是否允许重复 | 键类型 | 是否保留插入顺序 | 典型场景 |
|---|---|---|---|---|---|
Array | 一组值 | 允许 | 下标 | 是 | 列表、顺序处理 |
Object | 键值对 | 键不能重复 | 字符串 / Symbol | 不适合当“严格有序映射”理解 | 普通配置对象 |
Set | 一组值 | 不允许值重复 | - | 是 | 去重、成员判断 |
Map | 键值对 | 键不能重复 | 任意类型 | 是 | 字典、缓存、索引 |
2. Set 核心用法
2.1 创建与增删查
const set = new Set();
set.add(1);
set.add(2);
set.add(2);
console.log(set.size); // 2
console.log(set.has(1)); // true
set.delete(1);
console.log(set.has(1)); // false
set.clear();
console.log(set.size); // 0
常用 API:
new Set(iterable):创建集合add(value):添加值has(value):判断是否存在delete(value):删除值clear():清空size:元素个数
2.2 最常见用途:数组去重
const nums = [1, 2, 2, 3, 3, 3];
const uniqueNums = [...new Set(nums)];
console.log(uniqueNums); // [1, 2, 3]
这是 Set 最经典的写法。
因为:
Set天然去重- 展开运算符
...可以再转回数组
2.3 遍历 Set
const set = new Set(['a', 'b', 'c']);
for (const value of set) {
console.log(value);
}
set.forEach((value) => {
console.log(value);
});
注意:Set 遍历时拿到的就是“值本身”。
2.4 并集、交集、差集
JavaScript 面试里经常会让你手写这三个操作。
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);
const union = new Set([...a, ...b]);
const intersection = new Set([...a].filter((x) => b.has(x)));
const difference = new Set([...a].filter((x) => !b.has(x)));
console.log(union); // Set(5) {1, 2, 3, 4, 5}
console.log(intersection); // Set(1) {3}
console.log(difference); // Set(2) {1, 2}
2.5 Set 判断“相等”时要注意什么?
Set 判断值是否重复,不是简单的 === 字面理解,而是基于 SameValueZero 规则。
你可以先记住两个结论:
NaN在Set里会被认为和自己相等- 对象看的是引用地址,不是内容
const set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1
set.add({name: 'Tom'});
set.add({name: 'Tom'});
console.log(set.size); // 3
为什么后面会变成 3?因为这两个对象虽然内容一样,但它们是两个不同的引用。
Set 不能直接按“对象内容”去重如果你要对对象数组按某个字段去重,通常要结合 Map 或额外的 key 处理,而不是直接 new Set(arrayOfObjects)。
3. Map 核心用法
3.1 创建与增删查
const map = new Map();
map.set('name', 'Alice');
map.set('age', 18);
console.log(map.get('name')); // Alice
console.log(map.has('age')); // true
console.log(map.size); // 2
map.delete('age');
console.log(map.has('age')); // false
map.clear();
console.log(map.size); // 0
常用 API:
new Map(iterable):创建映射set(key, value):设置键值get(key):获取值has(key):判断键是否存在delete(key):删除键clear():清空size:键值对数量
3.2 为什么 Map 比 Object 更灵活?
看一个最直接的例子:
const objKey = {id: 1};
const map = new Map();
map.set(objKey, '用户信息');
console.log(map.get(objKey)); // 用户信息
如果换成普通对象:
const objKey = {id: 1};
const data = {};
data[objKey] = '用户信息';
console.log(data); // { '[object Object]': '用户信息' }
原因是:Object 的键最终会被转成字符串,而 Map 不会。
3.3 遍历 Map
const map = new Map([
['name', 'Alice'],
['age', 18],
]);
for (const [key, value] of map) {
console.log(key, value);
}
console.log([...map.keys()]); // ['name', 'age']
console.log([...map.values()]); // ['Alice', 18]
console.log([...map.entries()]); // [['name', 'Alice'], ['age', 18]]
3.4 高频场景:统计出现次数
const words = ['js', 'ts', 'js', 'react', 'js', 'ts'];
const countMap = new Map();
for (const word of words) {
countMap.set(word, (countMap.get(word) || 0) + 1);
}
console.log(countMap);
// Map(3) {'js' => 3, 'ts' => 2, 'react' => 1}
这个模式非常常见:
- 统计词频
- 统计接口错误码次数
- 统计用户点击次数
3.5 Map 与对象互转
const obj = {name: 'Alice', age: 18};
const map = new Map(Object.entries(obj));
console.log(map.get('name')); // Alice
const backToObj = Object.fromEntries(map);
console.log(backToObj); // { name: 'Alice', age: 18 }
- 如果只是一个简单 JSON 风格对象,
Object很自然 - 如果你需要频繁增删、遍历、保持插入顺序,或者键不是字符串,优先考虑
Map
4. Set / Map 为什么通常比 Array / Object 更适合查找?
先看一个直觉表:
| 操作 | Array | Set | Object | Map |
|---|---|---|---|---|
| 查某个值是否存在 | includes(),通常要线性扫描 | has(),通常更快 | in / hasOwnProperty() | has(),通常更快 |
| 添加元素 | push() | add() | 直接赋值 | set() |
| 删除元素 | splice() / 过滤 | delete() | delete obj[key] | delete() |
| 是否保序 | 是 | 是 | 不适合当严格有序结构 | 是 |
一般可以这样理解:
Array更擅长顺序处理Set更擅长成员判断与去重Object更像简单配置对象Map更适合真正的字典结构
工程里通常把 Set.has()、Map.get()、Map.has() 理解为平均接近 O(1) 的查找。实际性能还会受到引擎实现、数据规模、键类型等影响,但在语义和使用场景上,它们就是为“高效查找”设计的。
5. WeakSet 和 WeakMap:为什么前面要加 Weak?
WeakSet、WeakMap 适合处理对象关联信息,并且不想因为这层关联而阻止垃圾回收。
5.1 WeakMap 特点
- 键只能是对象
- 键是“弱引用语义”
- 不能遍历
- 没有
size
const wm = new WeakMap();
let user = {id: 1};
wm.set(user, {lastVisit: Date.now()});
console.log(wm.get(user));
user = null;
// 之后如果没有别的地方再引用原对象,它就可以被垃圾回收
典型用途:
- 给对象挂“私有元数据”
- 缓存某个对象对应的计算结果
- 避免因为缓存而造成内存泄漏
5.2 WeakSet 特点
- 只能存对象
- 成员是弱引用语义
- 不能遍历
- 没有
size
const visited = new WeakSet();
const obj = {};
visited.add(obj);
console.log(visited.has(obj)); // true
5.3 什么时候该用 Weak 版本?
如果你有这样的问题:
- “我想给对象做标记,但对象销毁后我不想手动清理”
- “我想缓存对象相关数据,但不想把对象强行留在内存里”
那就应该考虑 WeakMap / WeakSet。
6. 实战场景
6.1 对象数组按字段去重
Set 不能直接按对象内容去重,但可以配合 Map 按某个字段去重。
const users = [
{id: 1, name: 'Alice'},
{id: 2, name: 'Bob'},
{id: 1, name: 'Alice-重复'},
];
const uniqueUsers = [...new Map(users.map((user) => [user.id, user])).values()];
console.log(uniqueUsers);
// [
// { id: 1, name: 'Alice-重复' },
// { id: 2, name: 'Bob' }
// ]
这个技巧非常实用。
6.2 做“访问过”标记
const visited = new Set();
function visit(nodeId) {
if (visited.has(nodeId)) return;
visited.add(nodeId);
console.log('处理节点:', nodeId);
}
这在图遍历、树遍历、路由去重、防重复请求中都很常见。
6.3 缓存计算结果
const cache = new Map();
function fib(n) {
if (n <= 1) return n;
if (cache.has(n)) return cache.get(n);
const value = fib(n - 1) + fib(n - 2);
cache.set(n, value);
return value;
}
这就是最基础的记忆化缓存思路。
7. 常见坑
7.1 Map 不能像对象一样用点语法
const map = new Map();
map.set('name', 'Alice');
console.log(map.name); // undefined
console.log(map.get('name')); // Alice
7.2 Set / Map 不能直接 JSON 序列化成你想要的样子
console.log(JSON.stringify(new Set([1, 2, 3]))); // {}
console.log(JSON.stringify(new Map([['a', 1]]))); // {}
如果要序列化,通常先转成数组:
const setJson = JSON.stringify([...new Set([1, 2, 3])]);
const mapJson = JSON.stringify([...new Map([['a', 1]])]);
7.3 Object 的“存在性判断”容易踩原型链坑
const obj = Object.create({fromProto: true});
obj.own = true;
console.log('fromProto' in obj); // true
console.log(Object.hasOwn(obj, 'fromProto')); // false
如果你的需求是真正的字典结构,Map 往往更省心。
8. 怎么选:一套实用判断法
- 只是列表,允许重复,主要做顺序处理 →
Array - 想去重 / 判断是否存在 →
Set - 只是简单配置对象、JSON 数据 →
Object - 需要真正的键值映射,尤其键不是字符串 →
Map - 键必须是对象,且不想影响垃圾回收 →
WeakMap - 只想给对象打标记,且不想影响垃圾回收 →
WeakSet
你可以把它们记成一句话:
列表用 Array,去重用 Set,字典用 Map,简单配置用 Object,对象私有关系用 WeakMap / WeakSet。
高频面试题(含参考答案)
Q1:Set 和数组去重相比,优势是什么?
A: Set 天然保证值唯一,写法简洁,成员判断通常也比数组线性扫描更适合大规模数据场景。最常见写法是 const unique = [...new Set(arr)]。
Q2:Map 和 Object 的核心区别是什么?
A: Map 的键可以是任意类型,保留插入顺序,API 更适合做字典;Object 的键本质上主要是字符串 / Symbol,更适合表达普通配置数据。
Q3:为什么 new Set([{a: 1}, {a: 1}]) 不能去重?
A: 因为对象比较的是引用,不是内容。两个字面量对象虽然结构一样,但它们在内存里是两个不同对象,所以 Set 会把它们当成两个值。
Q4:WeakMap 为什么不能遍历?
A: 因为它的键是弱引用语义,键对象可能随时被垃圾回收。如果允许稳定遍历,就会和垃圾回收机制产生语义冲突,所以规范不提供遍历能力,也没有 size。
Q5:Map 会保证遍历顺序吗?
A: 会。Map 按插入顺序遍历,这也是它和 Object 很重要的区别之一。
Q6:什么时候应该优先用 Set?
A: 当你主要关注“某个值在不在”“值是否唯一”“是否访问过”这类问题时,优先考虑 Set。
Q7:什么时候应该优先用 Map?
A: 当你需要“键 → 值”的映射关系,并且希望键可以是任意类型、需要频繁增删查、希望遍历顺序稳定时,优先考虑 Map。