前言
当你开始写中大型前端代码时,数据结构不再是“学术题”,而是决定内存、安全与可维护性的工程决策。Set 与 Map 为我们提供了比数组和普通对象更明确、语义更清晰的集合类型;而 WeakMap、WeakSet 则在对象生命周期管理这一隐蔽但致命的问题上,给出了更优雅的答案——它们能在不干预垃圾回收的前提下,为对象贴上“状态标签”或保存元数据,从而避免潜在的内存泄漏。
一、Set:唯一值的集合
Set 是一种存储唯一值的集合结构,自动过滤重复项。与数组不同,Set 不会保留重复元素,而是直接忽略它们。
// 创建和操作 Set
const uniqueNumbers = new Set([1, 2, 3, 2, 1]);
uniqueNumbers.add(4); // 添加新值
uniqueNumbers.delete(2); // 删除值
console.log(uniqueNumbers.has(3)); // 检查值是否存在
console.log(uniqueNumbers.size); // 获取元素数量
最常见的应用场景是数组去重,只需一行代码即可实现:
const numbers = [1, 2, 3, 2, 1];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3]
二、Map:键值对的灵活存储
Map 是一种键值对集合,与普通对象不同,Map 允许使用任意类型作为键,包括对象、数组甚至 null。此外,Map 还保留了插入顺序,这对于某些需要有序数据的场景非常有用。
// 创建和操作 Map
const cache = new Map();
cache.set({ id: 1 }, '数据1');
cache.set(null, '特殊值');
cache.set('hello', 'world');
console.log(cache.get({ id: 1 })); // undefined,因为对象引用不同
console.log(cache.get(null)); // '特殊值'
console.log(cache.size); // 3
三、Set与Map的遍历
Set和Map都支持多种遍历方式:可通过 for...of、keys()、values()、entries() 或 forEach 遍历键值对,这里我简单举个例子。
const userMap = new Map([
['name', 'Alice'],
['age', 30],
['city', 'Shanghai']
]);
// 1. 最常用:for...of 遍历
for (const [key, value] of userMap) {
console.log(`Key: ${key}, Value: ${value}`);
}
// 输出:
// Key: name, Value: Alice
// Key: age, Value: 30
// Key: city, Value: Shanghai
// 2. entries() 方法遍历
for (const entry of userMap.entries()) {
console.log(entry); // [key, value] 数组
}
// 输出:
// ["name", "Alice"]
// ["age", 30]
// ["city", "Shanghai"]
四、WeakMap 和 WeakSet:弱引用的智能容器
WeakMap 和 WeakSet 是 ES6 引入的特殊数据结构,它们与普通 Map 和 Set 的主要区别在于使用弱引用存储对象。
弱引用机制
弱引用不会阻止垃圾回收机制回收对象。在 JavaScript 中,垃圾回收主要采用标记-清除算法。当垃圾回收器运行时,它会标记所有从全局作用域可达的对象。普通引用会将对象标记为"可达",从而阻止回收;而弱引用则不会参与这一标记过程,即使对象存在于 WeakMap 或 WeakSet 中,只要没有其他强引用指向它,垃圾回收器仍可以回收它。
这意味着,当对象不再被其他代码引用时,即使它存在于 WeakMap 或 WeakSet 中,也会被自动回收,不会造成内存泄漏。
WeakMap 的特性
WeakMap 是键值对的集合,但其键必须是对象或 Symbol(且不能是全局注册的 Symbol),而值可以是任意类型:
const weakCache = new WeakMap();
const obj = { id: 1 };
weakCache.set(obj, '元数据');
console.log(weakCache.get(obj)); // '元数据'
// 弱引用特性演示
obj = null; // 移除外部引用
// 弱引用不会阻止回收,此时弱引用中的对象会被自动回收
WeakSet 的特性
WeakSet 是一个值的集合,但其成员只能是对象或 Symbol,且所有成员都是弱引用:
const seenObjects = new WeakSet();
const obj1 = { id: 1 };
seenObjects.add(obj1);
console.log(seenObjects.has(obj1)); // true
// 弱引用特性演示
obj1 = null; // 移除外部引用
// 弱引用不会阻止回收,此时弱引用中的对象会被自动回收
需要注意的是:WeakMap 和 WeakSet 都不支持遍历操作,这意味着无法直接获取其中存储的所有对象或键值对,如果 WeakMap 的值中包含对键的强引用,仍然会导致循环引用和内存泄漏。
这也太复杂了,到底什么场景会用到 WeakMap/Set 呢?我们往下看
五、WeakMap 和 WeakSet 的典型应用场景
1. DOM 元素元数据存储
当需要为 DOM 元素存储额外元数据时,WeakMap 是理想选择:
const nodeMetadata = new WeakMap();
function setupNode(node) {
nodeMetadata.set(node, { initialized: true });
// 添加事件监听
node.addEventListener('click', handleClick);
}
function handleClick(event) {
const metadata = nodeMetadata.get(event.target);
if (metadata && metadata initialized) {
// 执行点击处理逻辑
}
}
当 DOM 元素被移除时,nodeMetadata 中的内容会因为是弱引用而直接变成空,也就是 nodeMetadata 被垃圾回收掉了其中的内存,避免了一个小小的内存泄漏。
2. 对象标记与状态管理
某些对象只需要被处理一次,或者只要知道“它是否已经处理过”即可,而并不关心这些对象的数量,也不需要遍历它们。
这类场景非常适合使用 WeakSet。
// 使用 WeakSet 记录“已经初始化过的对象”
const initializedServices = new WeakSet();
function initService(service) {
// 如果该对象已经初始化过,直接跳过
if (initializedServices.has(service)) return;
// ===== 一次性初始化逻辑 =====
service.connect();
service.startHeartbeat();
// 仅做标记:该对象已处理
initializedServices.add(service);
}
使用时:
let service = new Service();
initService(service);
initService(service); // 不会重复初始化
在这个例子中,WeakSet 只承担“标记”的职责:
它回答的问题只有一个 ——这个对象是否已经被初始化过?
当业务逻辑不再需要这个对象时,例如:
// 业务流程结束,不再使用该对象
service = null; // 仅用于演示:断开最后一个强引用
WeakSet 不会因为曾经标记过该对象而阻止垃圾回收,
对应的标记也会随对象一起自动消失。
实际开发中,对象更常因为超出作用域、组件卸载、DOM 被移除等原因而被自然丢弃,而不是手动写 = null。这里仅作为生命周期结束的示意。
顺带一提:
“对象被丢弃”并不是一个立即发生的动作,而是指:当对象不再被任何强引用访问时,它就具备了被垃圾回收的条件。
至于什么时候真正回收,由 JavaScript 引擎自行决定,我们无法知道垃圾回收机制什么时候执行。
写在最后
WeakMap 和 WeakSet 是 JavaScript 中处理对象引用的特殊工具,它们通过弱引用机制避免了常见的内存泄漏问题。理解弱引用的概念和 WeakMap/WeakSet 的特性,对于构建高性能、内存安全的应用至关重要。
在实际开发中,应根据数据的生命周期需求和访问模式,选择合适的引用类型和数据结构。对于需要与对象生命周期绑定的数据,WeakMap 和 WeakSet 是理想选择;而对于需要遍历或长期存在的数据,普通 Map 和 Set 更为合适。
掌握这些数据结构的使用技巧,不仅能够解决内存泄漏问题,还能提升代码的健壮性和性能。在处理 DOM 元素、组件实例或临时数据时,合理使用 WeakMap 和 WeakSet,可以让你的应用更加高效和安全。