各位观众老爷,大家好!我是今天的主讲人,咱们今天来聊聊 JavaScript 中一个低调但实用的小家伙——WeakSet
。
WeakSet
:我只想静静地看着你…消失
先问大家一个问题:在 JavaScript 中,我们如何追踪一个对象是否存在?最简单的办法就是用一个数组或者 Set
来存储这些对象。但是,问题来了!
let myObject = { name: "张三" };
let objectSet = new Set();
objectSet.add(myObject);
myObject = null; // 张三已经死了,但对象还在Set里面!
console.log(objectSet.has(myObject)); // false,但元素还在
console.log(objectSet.size); // 1
在这个例子中,即使我们将 myObject
设置为 null
,垃圾回收器也无法回收它,因为它仍然被 objectSet
引用着。这会导致内存泄漏!
这时候,WeakSet
就闪亮登场了。WeakSet
允许你存储对象的“弱引用”。这意味着,如果 WeakSet
中唯一对某个对象的引用是弱引用,那么当垃圾回收器认为这个对象应该被回收时,它就会被回收,即使 WeakSet
中仍然存在对它的引用。
let myObject = { name: "张三" };
let weakObjectSet = new WeakSet();
weakObjectSet.add(myObject);
myObject = null; // 张三已经死了,而且WeakSet也记不住他了!
// 稍后,垃圾回收器可能会回收这个对象
// 无法直接判断WeakSet是否还包含该对象(没有size属性)
// 无法直接遍历WeakSet中的对象(没有迭代器)
WeakSet
的特性
-
只能存储对象:
WeakSet
只能存储对象,不能存储原始值(如数字、字符串、布尔值等)。试图存储非对象会导致TypeError
。let weakSet = new WeakSet(); weakSet.add({ name: "李四" }); // OK // weakSet.add(123); // TypeError: Invalid value used in weak set
-
弱引用: 这是
WeakSet
的核心特性。它不会阻止垃圾回收器回收对象。 -
没有
size
属性:WeakSet
不提供size
属性,你无法知道它存储了多少个对象。 -
不可迭代:
WeakSet
不可迭代,你不能使用for...of
循环或者forEach
方法来遍历它。 -
仅有
add
、delete
、has
方法:WeakSet
只提供了这三个基本方法。
WeakSet
的应用场景
WeakSet
最适合用于以下场景:
-
追踪对象的存在性,但不阻止其被回收: 就像我们一开始举的例子,如果你需要跟踪某些对象的状态,但又不希望它们因为被跟踪而无法被回收,
WeakSet
是一个很好的选择。- 缓存机制: 可以用
WeakSet
来缓存一些计算结果,如果对象被回收,缓存也会自动失效。 - 事件监听器管理: 可以用来存储已经注册了事件监听器的 DOM 元素,当 DOM 元素被移除时,自动取消事件监听器。
- 缓存机制: 可以用
-
存储与对象相关的私有数据: 可以结合闭包和
WeakMap
(后面会讲到) 来实现对象的私有属性和方法。
代码示例:缓存机制
假设我们有一个耗时的计算函数:
function expensiveCalculation(obj) {
console.log("执行耗时计算...", obj);
// 模拟耗时计算
let result = 0;
for (let i = 0; i < 1000000; i++) {
result += Math.random();
}
return result;
}
我们希望对这个函数进行缓存,避免重复计算。使用 WeakSet
可以很方便地实现:
const calculationCache = new WeakMap(); // 使用WeakMap存储缓存结果
function cachedCalculation(obj) {
if (calculationCache.has(obj)) {
console.log("从缓存中获取...");
return calculationCache.get(obj);
} else {
const result = expensiveCalculation(obj);
calculationCache.set(obj, result);
return result;
}
}
let myObject1 = { id: 1 };
let myObject2 = { id: 2 };
console.log(cachedCalculation(myObject1)); // 第一次计算
console.log(cachedCalculation(myObject1)); // 从缓存中获取
console.log(cachedCalculation(myObject2)); // 第一次计算
myObject1 = null; // 对象1不再被引用
// 稍后,垃圾回收器可能会回收 myObject1
// 当 myObject1 被回收后,calculationCache 中对应的缓存也会自动失效
// 下次调用 cachedCalculation(myObject1) 会重新计算
setTimeout(() => {
let myObject1 = { id: 1 };
console.log(cachedCalculation(myObject1)); // 重新计算,因为之前的已经被回收了
}, 2000);
在这个例子中,我们使用 WeakMap
(类似于 WeakSet
,但可以存储键值对,键必须是对象) 来存储缓存结果。当 myObject1
被设置为 null
并且垃圾回收器回收它时,calculationCache
中对应的缓存也会自动失效。
WeakSet
与 Set
的区别
特性 | Set |
WeakSet |
---|---|---|
存储类型 | 任意类型的值 | 只能存储对象 |
引用类型 | 强引用 | 弱引用 |
size |
有 size 属性 |
没有 size 属性 |
可迭代性 | 可迭代 | 不可迭代 |
方法 | 提供了更多方法 | 只提供 add 、delete 、has |
WeakSet
与 WeakMap
的关系
WeakSet
存储的是对象的集合,而 WeakMap
存储的是键值对,其中键必须是对象。它们都使用弱引用,并且都不可迭代。
可以把 WeakSet
看作是 WeakMap
的一个特殊情况,其中所有的值都为 true
。
// 使用 WeakMap 模拟 WeakSet
class MyWeakSet {
constructor() {
this._weakMap = new WeakMap();
}
add(obj) {
this._weakMap.set(obj, true);
}
has(obj) {
return this._weakMap.has(obj);
}
delete(obj) {
return this._weakMap.delete(obj);
}
}
const myWeakSet = new MyWeakSet();
let obj1 = {};
myWeakSet.add(obj1);
console.log(myWeakSet.has(obj1)); // true
myWeakSet.delete(obj1);
console.log(myWeakSet.has(obj1)); // false
WeakSet
的局限性
-
调试困难: 由于
WeakSet
不可迭代,并且没有size
属性,因此很难调试。你无法直接查看WeakSet
中存储了哪些对象。 -
适用场景有限:
WeakSet
只适用于需要追踪对象存在性,但不阻止其被回收的特定场景。
总结
WeakSet
是 JavaScript 中一个非常有用的数据结构,它允许你存储对象的弱引用,避免内存泄漏。虽然它有一些局限性,但在合适的场景下,可以发挥很大的作用。
实际案例:DOM 元素事件监听器管理
假设我们有一个自定义组件,需要在 DOM 元素上注册事件监听器。当组件被销毁时,我们需要取消这些事件监听器,避免内存泄漏。
class MyComponent {
constructor(element) {
this.element = element;
this.listeners = new WeakSet(); // 存储已经注册的事件监听器
this.handleClick = this.handleClick.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.element.addEventListener("click", this.handleClick);
this.element.addEventListener("mouseover", this.handleMouseOver);
this.listeners.add(this.handleClick);
this.listeners.add(this.handleMouseOver);
}
handleClick(event) {
console.log("点击事件触发", event);
}
handleMouseOver(event) {
console.log("鼠标悬停事件触发", event);
}
destroy() {
// 移除事件监听器
this.listeners.forEach(listener => { //forEach is not a function
this.element.removeEventListener("click", this.handleClick);
this.element.removeEventListener("mouseover", this.handleMouseOver);
});
// 移除对 element 的引用, 让垃圾回收器可以回收
this.element = null;
}
}
const myElement = document.createElement("div");
myElement.textContent = "我的组件";
document.body.appendChild(myElement);
const myComponent = new MyComponent(myElement);
// 模拟组件销毁
setTimeout(() => {
myComponent.destroy();
document.body.removeChild(myElement); // 移除 DOM 元素
// myElement = null;
}, 5000);
// 点击 myElement,会触发 handleClick 事件
// 5 秒后,组件被销毁,事件监听器被移除
在这个例子中,我们使用 WeakSet
来存储已经注册的事件监听器。当组件被销毁时,我们遍历 listeners
,移除所有的事件监听器。如果 DOM 元素被移除,WeakSet
中的引用会自动失效,避免内存泄漏。
但是,上面的代码是有问题的。WeakSet
是不可迭代的,所以无法使用 forEach
方法。正确的做法是手动移除事件监听器,并且依赖于垃圾回收器在 DOM 元素被回收时自动释放内存。
class MyComponent {
constructor(element) {
this.element = element;
// this.listeners = new WeakSet(); // 不需要了
this.handleClick = this.handleClick.bind(this);
this.handleMouseOver = this.handleMouseOver.bind(this);
this.element.addEventListener("click", this.handleClick);
this.element.addEventListener("mouseover", this.handleMouseOver);
// this.listeners.add(this.handleClick);
// this.listeners.add(this.handleMouseOver);
}
handleClick(event) {
console.log("点击事件触发", event);
}
handleMouseOver(event) {
console.log("鼠标悬停事件触发", event);
}
destroy() {
// 移除事件监听器
this.element.removeEventListener("click", this.handleClick);
this.element.removeEventListener("mouseover", this.handleMouseOver);
// 移除对 element 的引用, 让垃圾回收器可以回收
this.element = null;
}
}
const myElement = document.createElement("div");
myElement.textContent = "我的组件";
document.body.appendChild(myElement);
const myComponent = new MyComponent(myElement);
// 模拟组件销毁
setTimeout(() => {
myComponent.destroy();
document.body.removeChild(myElement); // 移除 DOM 元素
// myElement = null;
}, 5000);
// 点击 myElement,会触发 handleClick 事件
// 5 秒后,组件被销毁,事件监听器被移除
在这个修正后的例子中,我们不再使用 WeakSet
来管理事件监听器。相反,我们在 destroy
方法中手动移除事件监听器,并设置 this.element = null
,以便垃圾回收器可以回收 DOM 元素。
总结:使用场景
虽然 WeakSet
不可迭代,使其在某些场景下使用不便,但它在以下场景仍然有用:
- 标记对象状态: 可以使用
WeakSet
来标记对象是否处于某种状态,例如 "已处理" 或 "已缓存"。 - 存储对象集合: 可以使用
WeakSet
来存储对象的集合,例如 "已注册的组件" 或 "已连接的用户"。
总而言之,WeakSet
是一个强大的工具,但需要谨慎使用,并充分了解其局限性。
最后的啰嗦
希望今天的讲座能帮助大家更好地理解和使用 WeakSet
。记住,没有银弹,选择合适的数据结构取决于你的具体需求。下次再见!