Skip to content

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)); // false

WeakMap 的限制

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]); // TypeError

WeakSet 详解

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)); // false

WeakSet 的限制

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 的对比

功能对比表

特性MapWeakMapSetWeakSet
键类型任意类型仅对象任意类型仅对象
值类型任意类型任意类型无值概念无值概念
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 都能发挥重要作用。