📚 推荐阅读我的专栏
👉 《JavaScript 》 系统解析语言核心与运行机制
👉 《JavaScript 手写篇 》 一行一行写出属于你的轮子!
欢迎关注,持续输出硬核前端技术内容。


WeakSet 是一种特殊的集合类型,与常规的 Set 相比,它在对象引用方面有根本性的不同和行为。理解 WeakSet 的关键在于理解其“弱引用”的特性。


1. 什么是 WeakSet?

WeakSet 是一种允许你唯一地、弱地持有对象的集合。这意味着:

  • 它只能存储对象,不能存储原始值(如字符串、数字、布尔值等)。
  • 对对象的引用是“弱引用”。这是 WeakSet 最核心的特性。

2. 核心特性:弱引用 (Weak Reference)

为了理解 WeakSet,必须先理解什么是弱引用。

  • 强引用(常规引用):这是我们在 JavaScript 中通常创建的引用。只要一个对象有一个强引用指向它,垃圾回收机制(GC)就不会回收它。

    let obj = { data: ‘test’ }; // 创建对象,变量 `obj` 是对它的强引用
    // 只要 `obj` 存在,这个对象就会一直存在于内存中
    obj = null; // 现在强引用消失了,垃圾回收器会在下次运行时回收这个对象
    
  • 弱引用:如果一个对象只有弱引用指向它,那么这个对象会被垃圾回收器视为“可回收的”。当垃圾回收机制运行时,如果该对象没有被任何强引用指向,即使它还在 WeakSet 中,它也会被自动从 WeakSet 中移除,并且其占用的内存会被释放

这个特性直接决定了 WeakSet 的 API 和行为。

3. 为什么需要 WeakSet?—— 主要用途

弱引用的特性使得 WeakSet 非常适合用于以下场景:

  1. 存储对象的“元数据”或“标签”
    你可以用 WeakSet 来标记一个对象,表明它具有某种特殊的属性或状态,而不需要修改对象本身。由于是弱引用,当你不再需要这个对象时,它会被自动清理,你无需担心从 WeakSet 中手动删除它,从而避免了内存泄漏。

    经典示例:跟踪已被处理过的对象

    // 一个 WeakSet,用于记录已经处理过的对象
    const processedObjects = new WeakSet();
    
    function processObject(obj) {
      if (processedObjects.has(obj)) {
        console.log(‘这个对象已经处理过了,跳过’);
        return;
      }
      // ... 进行一些复杂的处理逻辑 ...
      console.log(‘首次处理对象’);
      
      // 处理完后,将其加入 WeakSet
      processedObjects.add(obj);
    }
    
    let user1 = { name: ‘Alice’ };
    let user2 = { name: ‘Bob’ };
    
    processObject(user1); // 输出: “首次处理对象”
    processObject(user1); // 输出: “这个对象已经处理过了,跳过”
    processObject(user2); // 输出: “首次处理对象”
    
    // 如果之后我们不再需要 user1 对象了
    user1 = null; 
    // 在未来的某个时刻,垃圾回收器会自动将 user1 对象从 processedObjects WeakSet 中移除
    // 你不需要手动调用 processedObjects.delete(user1)
    
  2. DOM 元素管理
    在 Web 开发中,经常需要关联一些数据或状态到 DOM 元素上。使用 WeakSet 可以跟踪一组 DOM 元素(例如,哪些元素被禁用了、哪些正在执行动画等)。如果 DOM 元素从页面中被移除并被垃圾回收,它在 WeakSet 中的记录也会自动消失,完美避免了内存泄漏。

    const animatedElements = new WeakSet();
    
    function startAnimation(element) {
      if (animatedElements.has(element)) {
        return; // 已经在动画中,避免重复启动
      }
      element.classList.add(‘animating’);
      animatedElements.add(element);
      
      // 假设动画结束后会触发 ‘animationend’ 事件
      element.addEventListener(‘animationend’, () => {
        element.classList.remove(‘animating’);
        animatedElements.delete(element); // 手动删除是可以的,但不是必须的
      });
    }
    
    // 如果某个带有动画的 div 被移除了
    // const myDiv = document.getElementById(‘myDiv’);
    // myDiv.remove(); 
    // 即使没有调用 animatedElements.delete(myDiv),myDiv 也会从 WeakSet 中被自动清理
    

4. WeakSet 的 API 和方法

由于弱引用的不可预测性(垃圾回收的时机是不确定的),WeakSet 的 API 被设计得非常有限。

  • new WeakSet([iterable]): 构造函数。可以传入一个可迭代对象(例如数组),其元素必须是对象。

    const ws = new WeakSet([{a: 1}, document.body]);
    
  • .add(value): 向 WeakSet 添加一个对象 value。返回 WeakSet 本身,所以可以链式调用。

    const obj = {};
    ws.add(obj);
    
  • .has(value): 检查 value 对象是否存在于 WeakSet 中。返回布尔值。

    console.log(ws.has(obj)); // true
    
  • .delete(value): 从 WeakSet 中删除 value 对象。返回布尔值,表示删除是否成功。

    ws.delete(obj);
    console.log(ws.has(obj)); // false
    

特别注意,WeakSet 没有以下方法和属性:

  • .size: 无法获取元素数量。因为垃圾回收可能随时发生,.size 的值是不可靠的。
  • .clear(): 无法清空所有元素。
  • keys(), values(), entries(): 不可迭代。你无法遍历 WeakSet 中的内容。
  • forEach(): 没有用于遍历的方法。

这些限制都是为了保证其弱引用特性的安全性和一致性。

5. WeakSet 与 Set 的对比

特性 Set WeakSet
值的类型 任意类型(对象、原始值) 仅对象
引用类型 强引用 弱引用
垃圾回收 即使对象无其他引用,Set 仍持有它,阻止回收 对象无其他引用时,会被自动回收并从 WeakSet 移除
可遍历性 可迭代,支持 for...of, keys(), values() 不可迭代
.size 属性
.clear() 方法
主要用途 存储任意唯一值,需要遍历和查询大小 关联对象元数据,管理对象生命周期,避免内存泄漏

6. 总结与使用时机

使用 WeakSet 当:

  • 你需要存储与对象生命周期相关联的临时信息或标记。
  • 你不想手动管理集合中对象的添加和删除,希望自动处理。
  • 你主要的使用模式是检查一个对象是否存在于此集合中(.has())。
  • 你非常关心避免内存泄漏,尤其是在处理大量临时对象或 DOM 元素时。

不要使用 WeakSet 当:

  • 你需要遍历集合中的内容。
  • 你需要知道集合中有多少个元素。
  • 你需要存储原始值(字符串、数字等)。

WeakSet 是 JavaScript 中一个非常精巧的工具,它通过利用垃圾回收机制,为特定场景提供了高效且安全的内存管理方案。虽然它的 API 很简单,但其背后的思想对于编写健壮的 JavaScript 程序至关重要。

更多推荐