WeakMap / WeakSet:弱引用与内存泄漏防护
理解弱引用的概念
WeakMap 和 WeakSet 是 ES6 引入的两种新的数据结构,它们与 Map 和 Set 的主要区别在于使用了弱引用(Weak Reference)。弱引用不会阻止 JavaScript 引擎的垃圾回收机制回收对象,这对于防止内存泄漏非常重要。
强引用 vs 弱引用
javascript
// 强引用示例
let obj = { name: 'Alice' };
const map = new Map();
map.set(obj, 'value');
obj = null; // 清除原始引用
// 但对象仍然在内存中,因为 Map 保持着强引用
console.log(map.size); // 1
// 如果没有其他引用指向该对象,它将被垃圾回收javascript
// 弱引用示例
let obj2 = { name: 'Bob' };
const weakMap = new WeakMap();
weakMap.set(obj2, 'value');
obj2 = null; // 清除原始引用
// 对象可以被垃圾回收,WeakMap 中的条目也会被自动清除
// 无法直接检查 weakMap 的大小,因为它没有 size 属性WeakMap 详解
WeakMap 的基本用法
javascript
// 创建 WeakMap
const weakMap = new WeakMap();
// 创建键对象
const key1 = {};
const key2 = {};
// 设置键值对
weakMap.set(key1, 'value1');
weakMap.set(key2, 'value2');
// 获取值
console.log(weakMap.get(key1)); // value1
console.log(weakMap.get(key2)); // value2
// 检查是否存在某个键
console.log(weakMap.has(key1)); // true
// 删除键值对
weakMap.delete(key1);
console.log(weakMap.has(key1)); // falseWeakMap 的限制
javascript
// WeakMap 只能使用对象作为键
const weakMap = new WeakMap();
// 以下操作会抛出错误
// weakMap.set('string', 'value'); // TypeError
// weakMap.set(1, 'value'); // TypeError
// weakMap.set(true, 'value'); // TypeError
// weakMap.set(null, 'value'); // TypeError
// weakMap.set(undefined, 'value'); // TypeError
// 只能使用对象作为键
const obj = {};
weakMap.set(obj, 'value'); // 正确
// 没有 size 属性
// console.log(weakMap.size); // undefined
// 不能迭代
// for (let [key, value] of weakMap) {} // TypeError
// console.log([...weakMap]); // TypeErrorWeakSet 详解
WeakSet 的基本用法
javascript
// 创建 WeakSet
const weakSet = new WeakSet();
// 创建对象
const obj1 = {};
const obj2 = {};
// 添加元素
weakSet.add(obj1);
weakSet.add(obj2);
// 检查是否存在某个元素
console.log(weakSet.has(obj1)); // true
// 删除元素
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // falseWeakSet 的限制
javascript
// WeakSet 只能存储对象
const weakSet = new WeakSet();
// 以下操作会抛出错误
// weakSet.add('string'); // TypeError
// weakSet.add(1); // TypeError
// weakSet.add(true); // TypeError
// weakSet.add(null); // TypeError
// weakSet.add(undefined); // TypeError
// 只能存储对象
const obj = {};
weakSet.add(obj); // 正确
// 没有 size 属性
// console.log(weakSet.size); // undefined
// 不能迭代
// for (let value of weakSet) {} // TypeError
// console.log([...weakSet]); // TypeError内存管理与垃圾回收
弱引用如何防止内存泄漏
javascript
// 模拟内存泄漏场景
class EventEmitter {
constructor() {
// 使用普通 Map 存储事件监听器
this.listeners = new Map();
}
on(target, event, callback) {
if (!this.listeners.has(target)) {
this.listeners.set(target, new Map());
}
const targetListeners = this.listeners.get(target);
if (!targetListeners.has(event)) {
targetListeners.set(event, []);
}
targetListeners.get(event).push(callback);
}
emit(target, event, data) {
const targetListeners = this.listeners.get(target);
if (targetListeners && targetListeners.has(event)) {
targetListeners.get(event).forEach(callback => callback(data));
}
}
}
// 使用示例
const emitter = new EventEmitter();
let target = {};
emitter.on(target, 'click', () => console.log('clicked'));
console.log(emitter.listeners.size); // 1
target = null; // 清除原始引用
// 但 target 对象仍然在内存中,因为 EventEmitter 保持着引用
console.log(emitter.listeners.size); // 1 (仍然存在)javascript
// 使用 WeakMap 防止内存泄漏
class SafeEventEmitter {
constructor() {
// 使用 WeakMap 存储事件监听器
this.listeners = new WeakMap();
}
on(target, event, callback) {
if (!this.listeners.has(target)) {
this.listeners.set(target, new Map());
}
const targetListeners = this.listeners.get(target);
if (!targetListeners.has(event)) {
targetListeners.set(event, []);
}
targetListeners.get(event).push(callback);
}
emit(target, event, data) {
const targetListeners = this.listeners.get(target);
if (targetListeners && targetListeners.has(event)) {
targetListeners.get(event).forEach(callback => callback(data));
}
}
}
// 使用示例
const safeEmitter = new SafeEventEmitter();
let safeTarget = {};
safeEmitter.on(safeTarget, 'click', () => console.log('clicked'));
// 无法直接检查大小,但可以检查是否存在
console.log(safeEmitter.listeners.has(safeTarget)); // true
safeTarget = null; // 清除原始引用
// safeTarget 对象可以被垃圾回收,相关的监听器也会被自动清除
// console.log(safeEmitter.listeners.has(safeTarget)); // false实际应用场景
1. 私有数据存储
javascript
// 使用 WeakMap 存储私有数据
const privateData = new WeakMap();
class User {
constructor(name) {
// 将私有数据存储在 WeakMap 中
privateData.set(this, { name, ssn: '123-45-6789' });
}
getName() {
return privateData.get(this).name;
}
// 注意:不能直接访问 SSN,需要通过方法
getSSN() {
return privateData.get(this).ssn;
}
}
const user = new User('Alice');
console.log(user.getName()); // Alice
console.log(user.getSSN()); // 123-45-6789
// 私有数据不会出现在对象的属性中
console.log(Object.keys(user)); // []
console.log(Object.getOwnPropertyNames(user)); // []
// 当 user 对象被销毁时,相关的私有数据也会被自动清理2. DOM 节点关联数据
javascript
// 使用 WeakMap 存储 DOM 节点的关联数据
const domData = new WeakMap();
function attachData(element, data) {
domData.set(element, data);
}
function getData(element) {
return domData.get(element);
}
// 使用示例
const button = document.createElement('button');
attachData(button, { clickCount: 0, lastClick: null });
button.addEventListener('click', () => {
const data = getData(button);
data.clickCount++;
data.lastClick = new Date();
console.log(`Button clicked ${data.clickCount} times`);
});
// 当 button 元素被移除并垃圾回收时,相关数据也会被自动清理3. 对象状态管理
javascript
// 使用 WeakMap 管理对象状态
const objectStates = new WeakMap();
class GameObject {
constructor(id) {
this.id = id;
// 初始化状态
objectStates.set(this, {
active: true,
position: { x: 0, y: 0 },
health: 100
});
}
getState() {
return objectStates.get(this);
}
updatePosition(x, y) {
const state = objectStates.get(this);
state.position.x = x;
state.position.y = y;
}
takeDamage(damage) {
const state = objectStates.get(this);
state.health = Math.max(0, state.health - damage);
if (state.health === 0) {
state.active = false;
}
}
}
const player = new GameObject('player1');
player.updatePosition(10, 20);
player.takeDamage(25);
console.log(player.getState());
// { active: true, position: { x: 10, y: 20 }, health: 75 }
// 当 player 对象被销毁时,状态数据也会被自动清理4. 缓存机制
javascript
// 使用 WeakMap 实现对象缓存
const cache = new WeakMap();
function expensiveOperation(obj) {
// 检查缓存
if (cache.has(obj)) {
console.log('从缓存获取结果');
return cache.get(obj);
}
console.log('执行昂贵操作');
// 模拟昂贵的计算
const result = JSON.stringify(obj).length;
// 存储到缓存
cache.set(obj, result);
return result;
}
const data = { name: 'Alice', age: 30 };
console.log(expensiveOperation(data)); // 执行昂贵操作, 25
console.log(expensiveOperation(data)); // 从缓存获取结果, 25
// 当 data 对象被销毁时,缓存条目也会被自动清理WeakSet 的应用场景
对象标记
javascript
// 使用 WeakSet 标记对象
const processedObjects = new WeakSet();
function processObject(obj) {
// 检查对象是否已处理
if (processedObjects.has(obj)) {
console.log('对象已处理');
return;
}
console.log('处理对象');
// 执行处理逻辑
// ...
// 标记对象为已处理
processedObjects.add(obj);
}
const obj = {};
processObject(obj); // 处理对象
processObject(obj); // 对象已处理
// 当 obj 被销毁时,标记也会被自动清理临时对象跟踪
javascript
// 使用 WeakSet 跟踪临时对象
const temporaryObjects = new WeakSet();
class TemporaryManager {
createTemporary(data) {
const tempObj = { data, createdAt: Date.now() };
temporaryObjects.add(tempObj);
return tempObj;
}
isTemporary(obj) {
return temporaryObjects.has(obj);
}
}
const manager = new TemporaryManager();
const temp = manager.createTemporary({ message: 'Hello' });
console.log(manager.isTemporary(temp)); // true
console.log(temporaryObjects.has(temp)); // true
// 当 temp 对象被销毁时,WeakSet 中的条目也会被自动清理与 Map/Set 的对比
功能对比表
| 特性 | Map | WeakMap | Set | WeakSet |
|---|---|---|---|---|
| 键类型 | 任意类型 | 仅对象 | 任意类型 | 仅对象 |
| 值类型 | 任意类型 | 任意类型 | 无值概念 | 无值概念 |
| size 属性 | 有 | 无 | 有 | 无 |
| 可迭代 | 是 | 否 | 是 | 否 |
| 垃圾回收 | 阻止 | 允许 | 阻止 | 允许 |
使用场景选择
javascript
// 选择合适的集合类型
// 1. 需要存储键值对且键可以是任意类型 - 使用 Map
const config = new Map();
config.set('theme', 'dark');
config.set(1, 'first');
config.set(true, 'boolean key');
// 2. 需要存储键值对且键是对象,希望允许垃圾回收 - 使用 WeakMap
const metadata = new WeakMap();
const domElement = document.createElement('div');
metadata.set(domElement, { created: Date.now() });
// 3. 需要存储唯一值且值可以是任意类型 - 使用 Set
const uniqueIds = new Set();
uniqueIds.add(1);
uniqueIds.add(2);
uniqueIds.add(1); // 重复,会被忽略
// 4. 需要存储唯一对象且希望允许垃圾回收 - 使用 WeakSet
const activeComponents = new WeakSet();
const component = {};
activeComponents.add(component);最佳实践
1. 合理选择数据结构
javascript
// 根据使用场景选择合适的数据结构
// 需要持久化存储和迭代 - 使用 Map/Set
class PersistentCache {
constructor() {
this.cache = new Map();
}
set(key, value) {
this.cache.set(key, { value, timestamp: Date.now() });
}
get(key) {
const entry = this.cache.get(key);
return entry ? entry.value : undefined;
}
// 可以清理过期条目
cleanup(expirationTime) {
for (const [key, entry] of this.cache) {
if (Date.now() - entry.timestamp > expirationTime) {
this.cache.delete(key);
}
}
}
}
// 仅需要临时关联且允许垃圾回收 - 使用 WeakMap/WeakSet
class TemporaryAssociation {
constructor() {
this.associations = new WeakMap();
}
associate(obj, data) {
this.associations.set(obj, data);
}
getAssociation(obj) {
return this.associations.get(obj);
}
// 无需手动清理,垃圾回收会自动处理
}2. 避免误用
javascript
// 避免将 WeakMap/WeakSet 用于需要迭代或统计的场景
class BadExample {
constructor() {
this.items = new WeakSet();
}
// 错误:无法统计元素数量
getCount() {
// return this.items.size; // undefined
throw new Error('WeakSet 无法统计元素数量');
}
// 错误:无法遍历元素
getAllItems() {
// for (const item of this.items) {} // TypeError
throw new Error('WeakSet 无法遍历元素');
}
}
// 正确:需要迭代时使用 Set
class GoodExample {
constructor() {
this.items = new Set();
}
getCount() {
return this.items.size; // 正确
}
getAllItems() {
return [...this.items]; // 正确
}
}3. 内存敏感场景的应用
javascript
// 在内存敏感的场景中使用 WeakMap/WeakSet
class MemoryEfficientManager {
constructor() {
// 使用 WeakMap 存储对象的元数据
this.metadata = new WeakMap();
// 使用 WeakSet 跟踪活动对象
this.activeObjects = new WeakSet();
}
registerObject(obj, meta) {
this.metadata.set(obj, { ...meta, registeredAt: Date.now() });
this.activeObjects.add(obj);
}
getObjectMeta(obj) {
return this.metadata.get(obj);
}
isObjectActive(obj) {
return this.activeObjects.has(obj);
}
// 对象销毁时无需手动清理,WeakMap/WeakSet 会自动处理
}总结
WeakMap 和 WeakSet 通过使用弱引用机制,为 JavaScript 提供了防止内存泄漏的重要工具。它们特别适用于需要将数据与对象关联但又不想阻止对象被垃圾回收的场景。理解弱引用的概念以及 WeakMap/WeakSet 与 Map/Set 的区别,有助于我们在合适的场景中正确使用这些数据结构,从而编写出更加内存高效和健壮的 JavaScript 代码。在 DOM 操作、私有数据存储、缓存机制等场景中,WeakMap 和 WeakSet 都能发挥重要作用。