在 JavaScript 中,数据存储的结构选择直接影响代码的可读性和性能。除了传统的 Object 和 Array,ES6 引入了 Map、Set、WeakMap、WeakSet 四种新的集合类型,它们在键的类型、迭代行为、垃圾回收等方面有着本质的不同。本节帮你理清它们的区别,让你能根据实际场景做出正确的选择。
10.6.1 Map 与 Object 的对比与选型
在很长一段时间里,Object 几乎是唯一的键值对存储方式。但 Object 有一些先天局限性:键只能是字符串或 Symbol,迭代时需要处理原型链上的属性,属性顺序也不是严格保证的。Map 则为键值存储带来了一套更纯粹、更高效的解决方案。
核心区别
| 特性 | Map | Object |
|------|-----|--------|
| 键的类型 | 任意类型,包括对象、函数、原始值 | 只能是字符串或 Symbol |
| 键的顺序 | 严格按照插入顺序 | 基本保持插入顺序,但数字键会排到前面,历史遗留问题多 |
| 大小获取 | map.size 属性直接获取 | 需要 Object.keys(obj).length |
| 迭代 | 可直接用 for...of 遍历,有 keys()、values()、entries() 方法 | 需要 Object.keys() 转数组后遍历,或用 for...in 但会遍历原型链 |
| 性能 | 频繁增删键值对时性能更优 | 作为普通属性存取时很快,但大量增删或键值不确定时不如 Map |
| 原型链 | 没有默认原型属性,纯净安全 | 具有原型链,可能造成意外的键冲突(如 toString) |
选型建议
- 使用 Map 的场景:键的类型不确定(比如需要在对象之间建立映射关系);需要频繁增删键值对;需要保持插入顺序;需要直接获取大小或快速迭代。
- 使用 Object 的场景:简单、固定的键(通常为字符串),比如配置文件、JSON 数据的自然映射;或者需要定义方法、使用
this的语义;与 JSON 互转也很直接(Map 不能直接 JSON.stringify)。
代码示例
// Map: 以对象为键
const userRoles = new Map();
const user1 = { name: 'Alice' };
const user2 = { name: 'Bob' };
userRoles.set(user1, 'admin');
userRoles.set(user2, 'editor');
console.log(userRoles.get(user1)); // 'admin'
console.log(userRoles.size); // 2
// Object: 只能转成字符串键
const objMap = {};
objMap[user1] = 'admin'; // 实际键是 "[object Object]"
objMap[user2] = 'editor'; // 覆盖了上一个!
console.log(objMap); // { '[object Object]': 'editor' }
在生产环境中,如果你需要一个“字典”或“哈希表”,Map 通常是更安全、更现代的选择。
10.6.2 Set 与数组的对比与选型
Set 是值的集合,它的核心特点是成员的值是唯一的,没有重复项。与数组相比,Set 更适合用来管理无重复、无序的数据。
核心区别
| 特性 | Set | Array |
|------|-----|-------|
| 唯一性 | 自动去重,不允许重复值 | 允许重复,需手动去重 |
| 索引/顺序 | 值是无序的(虽然按插入顺序迭代),不能通过索引访问 | 有索引,可通过下标访问 |
| 大小获取 | set.size | array.length |
| 查找性能 | 检查某个值是否存在时,内部优化为接近 O(1) | 需要遍历,O(n)(indexOf、includes) |
| 增删 | add、delete,操作直接 | push、splice 等,根据位置操作 |
选型建议
- 使用 Set 的场景:需要存储唯一值(如标签、ID 去重);需要高效检查某元素是否存在;对顺序和索引无要求;用于数学集合运算(交集、并集、差集)。
- 使用 Array 的场景:需要按索引访问元素;需要保持元素顺序;需要对元素进行排序、切片、映射、过滤等数组特有操作。
代码示例
// Set 去重
const tags = new Set(['javascript', 'css', 'javascript', 'html']);
console.log([...tags]); // ['javascript', 'css', 'html']
console.log(tags.has('css')); // true
// 数组去重需要额外步骤
const dupArr = ['javascript', 'css', 'javascript', 'html'];
const uniqueArr = [...new Set(dupArr)];
实际开发中,Array 依然是最常用的数据结构,但当你明确需要“唯一值集合”这一语义时,Set 能让代码更简洁且性能更好。
10.6.3 WeakMap 与 WeakSet 的弱引用特性与适用场景
WeakMap 和 WeakSet 是 Map 和 Set 的“弱引用”版本。它们的行为与常规集合类似,但有两个关键差异:
- 键/值必须是对象(WeakMap 的键必须是对象,WeakSet 的值必须是对象)。
- 弱引用:这些对象引用不会阻止垃圾回收。如果其他地方不再引用该对象,它会被自动回收,WeakMap 对应的键值对或 WeakSet 中的元素也会随之消失。
由于这种弱引用特性,WeakMap 和 WeakSet 不可迭代,没有 size 属性,也不能用 for...of 遍历——因为其中的内容随时可能被回收,无法提供稳定的快照。
适用场景
- WeakMap:最常见的场景是存储对象的私有附加数据或元数据,且该数据的生命周期应与对象本身保持一致。例如,在 DOM 元素上关联状态数据,当 DOM 元素被移除后,关联数据自动被回收,防止内存泄漏。很多前端框架在实现响应式系统中的依赖追踪时,也会用到 WeakMap。
- WeakSet:用于收集对象,并检查某个对象是否在集合中,同时保证这些对象不被集合本身“钉”在内存中。典型用途是标记对象,比如记录某些 DOM 元素是否已经被处理过,或标记某对象处于某种特殊状态,当对象销毁时标记自动消失。
代码示例
// WeakMap: 存储 DOM 相关数据
const elementData = new WeakMap();
const div = document.querySelector('div');
elementData.set(div, { clickCount: 0 });
div.addEventListener('click', () => {
const data = elementData.get(div);
data.clickCount++;
});
// 当 div 从页面移除且不再引用时,div 和 elementData 中的条目都会被回收
// WeakSet: 标记已处理对象
const processed = new WeakSet();
function process(obj) {
if (processed.has(obj)) return;
// ... 处理逻辑
processed.add(obj);
}
与 Map/Set 的对比
| 特性 | WeakMap / WeakSet | Map / Set |
|------|-------------------|-----------|
| 引用类型 | 弱引用,不阻止 GC | 强引用,会阻止 GC |
| 键/值类型 | 必须为对象 | 任意类型 |
| 可迭代 | 不可迭代,无 size | 可迭代,有 size |
| 典型用途 | 关联元数据、避免内存泄漏 | 常规数据存储与操作 |
简单总结:Map 和 Set 是通用的键值集合和值集合;WeakMap 和 WeakSet 则是一种辅助性的内存管理工具,它们让你可以在不干预垃圾回收的前提下,建立对象与对象之间的短期关联。在日常开发中,别指望用 WeakMap 做数据存储,只有当对象的生命周期不由你控制,且需要避免内存泄漏时,才派它们上场。