Set 和 Map:为什么它们不是“对象和数组的语法糖”?该怎么选?
面试速答(30 秒版 TL;DR)
Set是“值的集合”,核心特点是 元素唯一;Map是“键值对集合”,核心特点是 键可以是任意值,不局限于字符串/Symbol。- 两者都保持 插入顺序,并且都天然可迭代。
Set最常见用途是去重、成员判断;Map最常见用途是缓存、索引、对象键映射。- 面试最高频是:
ObjectvsMap怎么选、Array去重为什么常用Set、WeakMap/WeakSet解决什么问题。
心智模型:它们是“集合结构”,不是普通对象的替身
一句话理解:
Set关心“有没有这个值”Map关心“这个键对应什么值”
它们解决的是:
- 更稳定的键集合管理
- 更明确的插入顺序
- 更自然的存在性判断
一、Set 的常用 API
const set = new Set([1, 2, 2, 3]);
console.log(set); // Set(3) {1, 2, 3}
| API | 作用 |
|---|---|
new Set(iterable?) | 创建 Set |
add(value) | 添加值 |
has(value) | 判断是否存在 |
delete(value) | 删除值 |
clear() | 清空 |
size | 元素个数 |
keys() / values() | 遍历值 |
entries() | 遍历 [value, value] |
forEach(fn) | 遍历 |
为什么 entries() 是 [value, value]
因为 Set 没有独立的 key 概念,为了和 Map/迭代协议接口保持一致,entries() 才返回成对结构。
二、Set 的典型场景
1) 数组去重
const unique = [...new Set([1, 2, 2, 3])];
// [1, 2, 3]
2) 快速成员判断
const whiteList = new Set(["admin", "editor"]);
whiteList.has("admin"); // true
3) 集合运算
const a = new Set([1, 2, 3]);
const b = new Set([3, 4, 5]);
const intersection = new Set([...a].filter((x) => b.has(x)));
const union = new Set([...a, ...b]);
const diff = new Set([...a].filter((x) => !b.has(x)));
三、Map 的常用 API
const map = new Map();
map.set("name", "Werry");
map.set({ id: 1 }, "object-key");
| API | 作用 |
|---|---|
new Map(iterable?) | 创建 Map |
set(key, value) | 设值 |
get(key) | 取值 |
has(key) | 判断键是否存在 |
delete(key) | 删除键 |
clear() | 清空 |
size | 键值对个数 |
keys() | 遍历键 |
values() | 遍历值 |
entries() | 遍历 [key, value] |
forEach(fn) | 遍历 |
四、Map 为什么比普通对象更像“真正的哈希表”
| 对比项 | Object | Map |
|---|---|---|
| 键类型 | 字符串 / Symbol | 任意值 |
| 默认原型属性 | 有 | 没有键污染问题 |
| 是否天然可迭代 | 否 | 是 |
| size 获取 | Object.keys(obj).length | map.size |
| 插入顺序 | 有自己的属性枚举规则 | 明确按插入顺序迭代 |
面试口径:
- 如果你要表达“字典对象”,且键可能不是字符串,优先
Map - 如果你要表达“固定结构的普通配置对象”,用
Object更自然
五、Set / Map 的相等性规则
它们判断键或值是否相同,通常基于 SameValueZero:
NaN和NaN视为相同+0和-0视为相同- 对象比较的仍然是引用地址
const set = new Set([NaN, NaN]);
set.size; // 1
const map = new Map();
map.set({}, 1);
map.get({}); // undefined,不是同一个引用
六、为什么对象做键时 Map 特别重要
const user = { id: 1 };
const cache = new Map();
cache.set(user, { posts: [] });
cache.get(user); // { posts: [] }
如果换成普通对象:
- 键会被转成字符串
- 两个不同对象很难安全地区分
七、WeakMap / WeakSet 解决什么问题
WeakMap
- 键必须是对象
- 键是弱引用
- 对象外部没人再引用时,可以被 GC 回收
典型用途:
- 给 DOM 节点、组件实例挂“附属元数据”
- 不想因为缓存把对象强行留在内存里
WeakSet
- 成员必须是对象
- 常用于“标记某对象处理过”
注意:
WeakMap/WeakSet不可枚举- 因为它们的内容可能随 GC 随时变化
八、典型使用场景
1) Set 去重与权限集合
- 去重标签
- 判断某 feature flag 是否启用
2) Map 做缓存与索引
- 请求缓存
- 节点到数据的映射
- LRU 的基础结构之一
典型题 & 标准答法
Q1:Map 和 Object 怎么选?
- 键是任意值、需要频繁增删查、需要保留插入顺序时,选
Map - 表达固定字段结构的普通数据时,选
Object
Q2:为什么数组去重常写成 [...new Set(arr)]?
因为:
- 表达直接
- 利用了
Set的唯一性 - 代码简洁,可读性好
Q3:WeakMap 为什么不能遍历?
因为它的键是弱引用,内容可能随着 GC 动态消失;如果还能稳定枚举,就和“弱引用”语义冲突了。
易错点/坑
Map用对象做键时,比较的是引用,不是结构相等。Set去重对象时,只能去掉“同一引用”的重复,不能做深度去重。- 把所有字典都改成
Map并不一定更好,固定结构对象用Object更自然。
速记要点(可背诵)
Set:唯一值集合,常用于去重和成员判断。Map:任意键的键值对集合,常用于缓存和索引。Object更像结构体,Map更像字典。WeakMap/WeakSet的核心价值是“不阻止垃圾回收”。