JavaScript WeakMap
什么是 WeakMap?
WeakMap 是 JavaScript ES6 (ECMAScript 2015) 引入的一种集合类型,它提供了一种存储键值对的方式,其中键必须是对象,且对键的引用是弱引用。
弱引用是指当对象仅被 WeakMap 引用时,它可以被垃圾回收机制回收。这与 Map 不同,Map 会保持对键的强引用,阻止垃圾回收。
WeakMap 的基本特性
WeakMap 具有以下几个重要特性:
- 键必须是对象:不能使用原始数据类型(如数字、字符串)作为键
- 弱引用:不会阻止垃圾回收机制回收作为键的对象
- 不可枚举:没有提供遍历方法(如
forEach
、keys
、values
) - 不支持 size 属性:无法获取 WeakMap 中的键值对数量
WeakMap 与 Map 的区别
让我们来看一下 WeakMap 和 Map 的主要区别:
特性 | WeakMap | Map |
---|---|---|
键类型 | 只能是对象 | 任意值 |
引用类型 | 弱引用 | 强引用 |
枚举性 | 不可枚举 | 可枚举 |
size 属性 | 不支持 | 支持 |
垃圾回收 | 键对象可被回收 | 键对象不可被回收 |
WeakMap 的基本用法
创建一个 WeakMap
// 创建一个空的 WeakMap
const wm = new WeakMap();
// 使用数组初始化 WeakMap
const key1 = {};
const key2 = {};
const wm2 = new WeakMap([
[key1, 'value1'],
[key2, 'value2']
]);
WeakMap 的方法
WeakMap 提供以下四个方法:
set(key, value)
: 设置键值对,返回 WeakMap 对象本身get(key)
: 获取指定键的值,如果键不存在则返回 undefinedhas(key)
: 检查是否存在指定的键,返回布尔值delete(key)
: 删除指定的键值对,成功返回 true,否则返回 false
让我们通过代码示例来看看这些方法的使用:
const wm = new WeakMap();
const obj = {};
// 设置键值对
wm.set(obj, 'Hello WeakMap');
// 获取值
console.log(wm.get(obj)); // 输出: "Hello WeakMap"
// 检查键是否存在
console.log(wm.has(obj)); // 输出: true
// 删除键值对
wm.delete(obj);
console.log(wm.has(obj)); // 输出: false
WeakMap 的垃圾回收机制
WeakMap 的一个核心特性是它对键的弱引用,这意味着当一个对象仅被 WeakMap 作为键引用时,JavaScript 的垃圾回收机制可以回收它。这在处理大量对象引用时特别有用。
让我们通过一个例子来说明:
let wm = new WeakMap();
// 创建一个作用域
{
let obj = {}; // 局部变量
wm.set(obj, "some data");
console.log(wm.get(obj)); // 输出: "some data"
}
// 此处 obj 已离开作用域
// 在未来某个时间点,垃圾回收机制会回收 obj 对象
// 此时 wm 中对应的键值对也会被移除
// 由于 WeakMap 不提供枚举方法和 size 属性
// 我们无法直接验证 obj 是否已被回收
// 但我们知道当 obj 被回收后,它在 wm 中的条目也将被移除
无法直接观察到 WeakMap 中的键何时被垃圾回收,因为垃圾回收是由 JavaScript 引擎在后台自动执行的。
WeakMap 的实际应用场景
WeakMap 在特定场景下有着独特的优势,下面介绍几个常见的应用场景:
1. 关联数据存储(不造成内存泄漏)
// 使用 WeakMap 存储 DOM 元素关联的数据
const domData = new WeakMap();
function processElement(element) {
// 为元素添加关联数据
let data = domData.get(element) || {};
data.clickCount = (data.clickCount || 0) + 1;
domData.set(element, data);
// 使用关联数据
console.log(`Element clicked ${data.clickCount} times`);
}
// 使用示例
const button = document.getElementById('myButton');
button.addEventListener('click', () => processElement(button));
// 当 DOM 元素被移除时,相关数据会被垃圾回收
// 不会造成内存泄漏
2. 实现私有属性
// 使用 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) {
const data = privateData.get(this);
data.name = name;
privateData.set(this, data);
}
}
const user = new User('Alice', 25);
console.log(user.getName()); // 输出: "Alice"
user.setName('Bob');
console.log(user.getName()); // 输出: "Bob"
// 无法从外部直接访问私有数据
console.log(user.name); // 输出: undefined
3. 缓存计算结果
// 使用 WeakMap 缓存函数计算结果
const cache = new WeakMap();
function expensiveOperation(obj) {
if (cache.has(obj)) {
console.log('返回缓存结果');
return cache.get(obj);
}
console.log('执行复杂计算');
// 模拟耗时计算
const result = Object.keys(obj).length * 100;
// 存储结果
cache.set(obj, result);
return result;
}
const testObj = { a: 1, b: 2, c: 3 };
console.log(expensiveOperation(testObj)); // 输出: "执行复杂计算" 和 300
console.log(expensiveOperation(testObj)); // 输出: "返回缓存结果" 和 300
// 如果 testObj 不再被引用,缓存结果会被垃圾回收
最佳实践与注意事项
使用 WeakMap 时,有一些最佳实践可以遵循:
- 当需要将额外数据与对象关联,但又不想阻止对象被垃圾回收时,使用 WeakMap
- 当需要遍历键值对或获取集合大小时,应使用 Map 而非 WeakMap
- 理解 WeakMap 中的键必须是对象,不能是原始值
- 不要依赖 WeakMap 进行确定性的垃圾回收,因为垃圾回收的时机是由 JavaScript 引擎决定的
WeakMap 不能用来存储必须保留的重要数据,因为键对象可能会随时被垃圾回收。
总结
WeakMap 是 JavaScript 的一种特殊集合类型,它提供了一种存储键值对的方式,其中键必须是对象且是弱引用。WeakMap 的主要优势在于能够避免内存泄漏,特别适用于需要将数据与对象关联但又不想阻止对象被垃圾回收的场景。
尽管 WeakMap 不如 Map 功能丰富(不可枚举,没有 size 属性),但在特定场景下,如实现私有属性、缓存计算结果和关联数据存储等,它具有独特的优势。
练习与进一步学习
为了巩固对 WeakMap 的理解,可以尝试以下练习:
- 创建一个使用 WeakMap 跟踪对象实例数量的系统
- 使用 WeakMap 实现一个简单的记忆化函数(memoization function)
- 比较使用 Map 和 WeakMap 在处理大量对象引用时的内存占用差异
进一步学习资源
通过深入理解 WeakMap,你将能够更有效地处理对象关联数据,并避免常见的内存泄漏问题。