2025年3月7日大约 11 分钟
我将为您完善WeakMap与WeakSet的文档,详细介绍这两种弱引用集合类型的特性和应用场景。
---
title: WeakMap与WeakSet
icon: javascript
order: 5
---
# WeakMap与WeakSet
WeakMap和WeakSet是Map和Set的"弱引用"版本,它们不会阻止键对象被垃圾回收。本文将介绍这两种集合类型的特性、使用场景以及与普通Map/Set的区别。
## 弱引用的概念
在JavaScript中,通常对象的引用是"强引用",只要存在至少一个强引用,对象就不会被垃圾回收。而"弱引用"不会阻止垃圾回收器回收它所引用的对象。
WeakMap和WeakSet中的键(WeakMap)或值(WeakSet)是弱引用,这意味着:
- 如果对象仅被WeakMap或WeakSet引用,它可以被垃圾回收
- 当对象被垃圾回收后,相应的WeakMap或WeakSet条目会自动被移除
## WeakMap
### WeakMap的基本特性
WeakMap是一种键值对集合,其中键必须是对象,值可以是任意类型。WeakMap与Map相比有以下特点:
1. **键的限制**:WeakMap的键只能是对象,不能是原始值(如字符串、数字等)
2. **弱引用**:WeakMap对键是弱引用,不会阻止垃圾回收
3. **不可枚举**:WeakMap不可迭代,没有`keys()`、`values()`、`entries()`方法
4. **无大小信息**:WeakMap没有`size`属性
5. **有限的方法**:只有`get()`、`set()`、`has()`、`delete()`方法
### 创建和使用WeakMap
```javascript
// 创建空WeakMap
const weakMap = new WeakMap();
// 创建带有初始值的WeakMap
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const initialWeakMap = new WeakMap([
[obj1, '对象1的数据'],
[obj2, '对象2的数据']
]);
// 添加键值对
const user = { name: '张三' };
weakMap.set(user, { visits: 1, lastVisit: new Date() });
// 获取值
console.log(weakMap.get(user)); // { visits: 1, lastVisit: ... }
// 检查键是否存在
console.log(weakMap.has(user)); // true
// 删除键值对
weakMap.delete(user);
console.log(weakMap.has(user)); // false
// 尝试使用非对象键会抛出错误
try {
weakMap.set('string', 'value'); // TypeError: Invalid value used as weak map key
} catch (e) {
console.error(e.message);
}
WeakMap的垃圾回收行为
let obj = { id: 1 };
const weakMap = new WeakMap();
weakMap.set(obj, '与对象关联的数据');
console.log(weakMap.get(obj)); // '与对象关联的数据'
// 移除对obj的引用
obj = null;
// 此时,如果没有其他引用指向原始对象,它将被垃圾回收
// 垃圾回收后,weakMap中的相应条目也会被自动移除
// 但我们无法直接观察到这一点,因为WeakMap没有size属性和迭代方法
WeakMap的应用场景
1. 关联数据存储
WeakMap最常见的用途是将额外数据关联到对象,而不影响对象的生命周期。
// 存储DOM元素的额外数据
const elementData = new WeakMap();
function setupElement(element, data) {
elementData.set(element, data);
}
const button = document.getElementById('myButton');
setupElement(button, { clickCount: 0 });
button.addEventListener('click', () => {
const data = elementData.get(button);
data.clickCount++;
console.log(`按钮被点击了${data.clickCount}次`);
});
// 当button元素被移除时,相关数据会被自动垃圾回收
2. 私有数据存储
WeakMap可以用于实现类的私有属性。
// 使用WeakMap实现私有属性
const privateData = new WeakMap();
class User {
constructor(name, age) {
privateData.set(this, {
name,
age
});
}
getName() {
return privateData.get(this).name;
}
getAge() {
return privateData.get(this).age;
}
setName(name) {
privateData.get(this).name = name;
}
}
const user = new User('张三', 30);
console.log(user.getName()); // '张三'
user.setName('李四');
console.log(user.getName()); // '李四'
// 无法直接访问私有数据
console.log(user.name); // undefined
3. 缓存和记忆化
WeakMap适合用于缓存计算结果,当对象不再使用时,缓存会自动清理。
// 使用WeakMap实现记忆化函数
const cache = new WeakMap();
function memoize(fn) {
return function(obj) {
if (!cache.has(obj)) {
console.log('计算结果...');
const result = fn(obj);
cache.set(obj, result);
} else {
console.log('使用缓存结果...');
}
return cache.get(obj);
};
}
const calculateObjectProperties = memoize(obj => {
// 假设这是一个耗时的计算
return Object.keys(obj).length;
});
const obj = { a: 1, b: 2, c: 3 };
console.log(calculateObjectProperties(obj)); // 计算结果... 3
console.log(calculateObjectProperties(obj)); // 使用缓存结果... 3
WeakSet
WeakSet的基本特性
WeakSet是一种值的集合,其中的值必须是对象。WeakSet与Set相比有以下特点:
- 值的限制:WeakSet只能存储对象,不能存储原始值
- 弱引用:WeakSet对值是弱引用,不会阻止垃圾回收
- 不可枚举:WeakSet不可迭代,没有
keys()
、values()
、entries()
方法 - 无大小信息:WeakSet没有
size
属性 - 有限的方法:只有
add()
、has()
、delete()
方法
创建和使用WeakSet
// 创建空WeakSet
const weakSet = new WeakSet();
// 创建带有初始值的WeakSet
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const initialWeakSet = new WeakSet([obj1, obj2]);
// 添加值
const user = { name: '张三' };
weakSet.add(user);
// 检查值是否存在
console.log(weakSet.has(user)); // true
// 删除值
weakSet.delete(user);
console.log(weakSet.has(user)); // false
// 尝试使用非对象值会抛出错误
try {
weakSet.add('string'); // TypeError: Invalid value used in weak set
} catch (e) {
console.error(e.message);
}
WeakSet的垃圾回收行为
let obj = { id: 1 };
const weakSet = new WeakSet();
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
// 移除对obj的引用
obj = null;
// 此时,如果没有其他引用指向原始对象,它将被垃圾回收
// 垃圾回收后,weakSet中的相应条目也会被自动移除
// 但我们无法直接观察到这一点,因为WeakSet没有size属性和迭代方法
WeakSet的应用场景
1. 对象标记
WeakSet最常见的用途是标记对象,而不影响对象的生命周期。
// 使用WeakSet标记已处理的对象
const processedObjects = new WeakSet();
function processObject(obj) {
if (processedObjects.has(obj)) {
console.log('对象已处理,跳过');
return;
}
// 处理对象
console.log('处理对象:', obj);
// 标记为已处理
processedObjects.add(obj);
}
const obj = { data: 'some data' };
processObject(obj); // 处理对象: { data: 'some data' }
processObject(obj); // 对象已处理,跳过
2. 防止循环引用
WeakSet可以用于防止递归算法中的循环引用问题。
// 防止循环引用导致的无限递归
function traverseObject(obj, visited = new WeakSet()) {
// 检查对象是否已访问过
if (visited.has(obj)) {
console.log('检测到循环引用,跳过');
return;
}
// 标记对象为已访问
visited.add(obj);
// 处理对象的属性
for (const key in obj) {
if (typeof obj[key] === 'object' && obj[key] !== null) {
console.log(`递归处理属性: ${key}`);
traverseObject(obj[key], visited);
}
}
}
// 创建带有循环引用的对象
const obj1 = { name: '对象1' };
const obj2 = { name: '对象2', ref: obj1 };
obj1.ref = obj2; // 创建循环引用
traverseObject(obj1); // 不会导致无限递归
3. 实现对象唯一性
WeakSet可以用于确保对象的唯一性,同时允许垃圾回收。
// 使用WeakSet实现对象唯一性
class UniqueObjects {
constructor() {
this.objects = new WeakSet();
}
add(obj) {
if (this.objects.has(obj)) {
return false; // 对象已存在
}
this.objects.add(obj);
return true; // 成功添加
}
has(obj) {
return this.objects.has(obj);
}
}
const uniqueObjs = new UniqueObjects();
const obj1 = { id: 1 };
const obj2 = { id: 2 };
const obj3 = obj1; // 与obj1相同的引用
console.log(uniqueObjs.add(obj1)); // true
console.log(uniqueObjs.add(obj2)); // true
console.log(uniqueObjs.add(obj3)); // false(obj3与obj1是同一个对象)
WeakMap与WeakSet的局限性
使用WeakMap和WeakSet时需要注意以下局限性:
- 不可迭代:无法获取所有键或值的列表
- 无法清空:没有
clear()
方法(但可以通过创建新实例来实现) - 无法获取大小:没有
size
属性 - 键/值限制:只能使用对象作为键/值
- 垃圾回收时机不确定:无法控制或观察垃圾回收的具体时机
// WeakMap和WeakSet的局限性示例
const weakMap = new WeakMap();
const weakSet = new WeakSet();
// 无法获取所有键或值
console.log(weakMap.keys); // undefined
console.log(weakSet.values); // undefined
// 无法获取大小
console.log(weakMap.size); // undefined
console.log(weakSet.size); // undefined
// 无法清空(需要创建新实例)
// weakMap.clear(); // TypeError: weakMap.clear is not a function
// 只能使用对象作为键/值
try {
weakMap.set(42, 'value'); // 错误
} catch (e) {
console.error('WeakMap键错误:', e.message);
}
try {
weakSet.add('string'); // 错误
} catch (e) {
console.error('WeakSet值错误:', e.message);
}
WeakMap/WeakSet与Map/Set的比较
特性 | WeakMap/WeakSet | Map/Set |
---|---|---|
键/值类型 | 只能是对象 | 任何类型 |
引用类型 | 弱引用 | 强引用 |
迭代性 | 不可迭代 | 可迭代 |
大小获取 | 不支持 | 支持size 属性 |
清空操作 | 不支持clear() | 支持clear() |
垃圾回收 | 不阻止GC | 阻止GC |
用途 | 关联数据、对象标记 | 通用数据存储 |
内存管理与垃圾回收
强引用与内存泄漏
使用普通Map/Set可能导致内存泄漏,特别是在长时间运行的应用中:
// 使用Map可能导致内存泄漏
const cache = new Map();
function processData(data) {
// 假设data是一个大对象
cache.set(data, { processed: true });
// 处理完成后,即使不再需要data,
// 它仍然被cache引用,不会被垃圾回收
}
// 随着时间推移,cache会不断增长,占用越来越多的内存
使用WeakMap避免内存泄漏
WeakMap可以有效避免这类内存泄漏问题:
// 使用WeakMap避免内存泄漏
const cache = new WeakMap();
function processData(data) {
// 假设data是一个对象
cache.set(data, { processed: true });
// 当data不再被其他地方引用时,
// 它会被垃圾回收,同时cache中的相应条目也会被移除
}
// 即使处理了大量对象,cache的大小也会随着对象的垃圾回收而自动减少
垃圾回收的不确定性
需要注意的是,JavaScript的垃圾回收是自动且不可预测的:
let obj = { data: 'some data' };
const weakMap = new WeakMap();
weakMap.set(obj, 'metadata');
// 移除强引用
obj = null;
// 垃圾回收可能会立即发生,也可能稍后发生
// 我们无法确切知道weakMap中的条目何时被移除
实际应用示例
示例1:实现可缓存的API调用
// 使用WeakMap实现可缓存的API调用
const apiCache = new WeakMap();
async function fetchData(requestObject) {
// 检查缓存
if (apiCache.has(requestObject)) {
const cachedData = apiCache.get(requestObject);
// 检查缓存是否过期
if (Date.now() - cachedData.timestamp < 60000) { // 1分钟缓存
console.log('使用缓存数据');
return cachedData.data;
}
}
// 发起API请求
console.log('发起新请求');
try {
const response = await fetch(requestObject.url, requestObject.options);
const data = await response.json();
// 更新缓存
apiCache.set(requestObject, {
data,
timestamp: Date.now()
});
return data;
} catch (error) {
console.error('请求失败:', error);
throw error;
}
}
// 使用示例
const request1 = {
url: 'https://api.example.com/data',
options: { method: 'GET' }
};
// 第一次调用会发起请求
fetchData(request1).then(data => console.log('数据1:', data));
// 短时间内再次调用会使用缓存
setTimeout(() => {
fetchData(request1).then(data => console.log('数据2:', data));
}, 10000);
// 当request1对象不再被引用时,相关缓存会被自动清理
示例2:DOM元素事件跟踪
// 使用WeakMap跟踪DOM元素的事件处理
const eventTracker = new WeakMap();
function trackEvents(element) {
if (!eventTracker.has(element)) {
eventTracker.set(element, {
clicks: 0,
hovers: 0,
lastInteraction: null
});
// 添加事件监听器
element.addEventListener('click', () => {
const stats = eventTracker.get(element);
stats.clicks++;
stats.lastInteraction = 'click';
});
element.addEventListener('mouseover', () => {
const stats = eventTracker.get(element);
stats.hovers++;
stats.lastInteraction = 'hover';
});
}
return {
getStats: () => eventTracker.get(element)
};
}
// 使用示例
const button = document.getElementById('myButton');
const tracker = trackEvents(button);
// 稍后检查统计信息
setTimeout(() => {
const stats = tracker.getStats();
console.log(`点击次数: ${stats.clicks}, 悬停次数: ${stats.hovers}`);
}, 5000);
// 当button元素被移除时,相关统计数据会被自动清理
示例3:使用WeakSet实现对象池
// 使用WeakSet实现对象池
class ObjectPool {
constructor() {
this.available = [];
this.inUse = new WeakSet();
}
acquire() {
let obj;
if (this.available.length > 0) {
obj = this.available.pop();
console.log('重用对象');
} else {
obj = this.createNewObject();
console.log('创建新对象');
}
this.inUse.add(obj);
return obj;
}
release(obj) {
if (this.inUse.has(obj)) {
this.inUse.delete(obj);
this.available.push(obj);
console.log('对象返回池中');
return true;
}
console.log('对象不在使用中');
return false;
}
createNewObject() {
// 创建新对象的逻辑
return { id: Math.random() };
}
}
// 使用示例
const pool = new ObjectPool();
const obj1 = pool.acquire();
const obj2 = pool.acquire();
pool.release(obj1);
const obj3 = pool.acquire(); // 会重用obj1
// 当对象不再被引用时,inUse集合中的相应条目会被自动清理
最佳实践
何时使用WeakMap/WeakSet
- 关联元数据:当需要将额外数据关联到对象,但不想影响对象的生命周期时。
- 缓存场景:当缓存与对象相关的计算结果,并希望在对象不再使用时自动清理缓存。
- 私有数据:当实现类的私有属性或方法时。
- 对象标记:当需要标记对象但不影响其生命周期时。
- 防止内存泄漏:在长时间运行的应用中,特别是涉及DOM元素时。
何时使用Map/Set
- 需要迭代:当需要遍历所有键或值时。
- 需要获取大小:当需要知道集合中有多少项时。
- 使用原始值:当键或值是字符串、数字等原始类型时。
- 需要清空操作:当需要一次性清除所有项时。
- 需要持久存储:当希望保留所有项,不管它们是否还被其他地方引用。
总结
WeakMap和WeakSet是JavaScript中处理对象引用的强大工具,它们通过弱引用机制帮助避免内存泄漏,特别适合以下场景:
WeakMap:
- 将额外数据关联到对象
- 实现私有属性
- 缓存计算结果
- 跟踪对象状态
WeakSet:
- 标记已处理的对象
- 防止循环引用
- 实现对象唯一性
- 对象池管理
虽然WeakMap和WeakSet有一些局限性(如不可迭代、只能使用对象作为键/值),但在内存管理和防止内存泄漏方面,它们提供了普通Map和Set无法替代的功能。在开发长时间运行的应用、处理大量对象或与DOM交互时,合理使用这些弱引用集合类型可以显著提高应用的性能和稳定性。