务必谨慎使用JS WeakRef弱引用

这篇文章发布于 2026年04月13日,星期一,16:01,归类于 JS API。 阅读 112 次, 今日 111 次 没有评论

 

一、weak三剑客

JS语言中的弱类型除了本文要介绍的WeakRef,还有WeakMap和WeakSet,其中WeakMap我5年前有介绍过,参见“JS WeakMap应该什么时候使用”。

WeakMap和WeakSet支持很多很多年了,无需顾忌任何兼容性问题,但是WeakRef则是前几年刚支持的特性。

WeakRef的兼容性

三者的本质区别

用一句话说明WeakMap、WeakSet和WeakRef的区别,那就是:

WeakMap 和 WeakSet 是集合类数据结构,弱引用是它们管理成员的方式;WeakRef 是对单个对象的弱引用包装器,让你能主动检查对象是否还活着。

用更加通俗的话解释就是:

  • WeakMap:我想给对象贴标签/存数据,但不阻止它被回收
  • WeakSet:我想记住哪些对象出现过,但不阻止它们被回收
  • WeakRef:我想保留一个对象的引用,但允许它随时被回收,用时再检查

下面案例时刻:

1. WeakMap —— 以对象为键的键值映射

const wm = new WeakMap();
let obj = { name: 'Alice' };

wm.set(obj, '额外数据');   // 键必须是对象
console.log(wm.get(obj));  // '额外数据'

obj = null; // obj 被回收后,WeakMap 中对应的条目也会自动消失

补充说明:

  • 必须是对象(不能是原始值)
  • 值可以是任意类型
  • 当键对象没有其他引用时,键值对会被 GC 自动回收
  • 常用于:DOM 节点关联数据、类的私有数据存储

2. WeakSet —— 对象的弱引用集合

const ws = new WeakSet();
let obj = { id: 1 };

ws.add(obj);              // 只能添加对象
console.log(ws.has(obj)); // true

obj = null; // obj 被回收后,WeakSet 中的条目也自动消失

补充说明:

  • 只能存储对象,不能存原始值
  • 只有 addhasdelete 三个方法
  • 常用于:标记对象是否”已访问”、”已处理”、防止循环引用

3. WeakRef —— 对单个对象的弱引用

let obj = { data: 'heavy resource' };
const ref = new WeakRef(obj);

// 通过 .deref() 获取目标对象
console.log(ref.deref());        // { data: 'heavy resource' }
console.log(ref.deref()?.data);  // 'heavy resource'

obj = null;
// 某次 GC 之后...
console.log(ref.deref()); // undefined(对象已被回收)

要点说明:

  • 包装单个对象,不是集合
  • 通过 .deref() 取回对象,如果已被 GC 回收则返回 undefined
  • 通常搭配 FinalizationRegistry 使用,在对象被回收时执行清理回调
  • 常用于:缓存(对象在则命中缓存,被回收则重新创建)

二、JS WeakRef 弱引用经典案例

列举几个适合使用JS WeakRef的场景,希望可以让大家对WeakRef的作用有更加深刻的理解。

1. 缓存场景

const cache = new Map();

function getCached(key, createFn) {
  const ref = cache.get(key);
  const cached = ref?.deref();

  if (cached) return cached; // 缓存命中

  // 缓存未命中或已被 GC 回收,重新创建
  const newObj = createFn();
  cache.set(key, new WeakRef(newObj));
  return newObj;
}

这里 WeakRef 允许缓存的大对象在内存紧张时被自动回收,避免内存泄漏,个人观点,缓存处理是WeakRef最具代表性的应用场景。

2. DOM 元素不泄漏的临时引用

先看DOM元素删除,但是内存依然占用的例子:

// 典型泄漏场景:DOM 节点已从页面移除,但 JS 仍持有强引用
const detachedNodes = [];

function addItem() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  detachedNodes.push(div); // 强引用
}

function removeItem(div) {
  document.body.removeChild(div);
  // 虽然从 DOM 树移除了,但 detachedNodes 数组还引用着它
  // → GC 无法回收 → 内存泄漏!
}

WeakRef可以解决这个问题:

const nodeRefs = [];

function addItem() {
  const div = document.createElement('div');
  document.body.appendChild(div);
  nodeRefs.push(new WeakRef(div)); // 弱引用,不阻止 GC
}

function doSomethingWithNodes() {
  for (const ref of nodeRefs) {
    const node = ref.deref();
    if (node) {
      // 节点还活着,正常操作
      node.style.color = 'red';
    } else {
      // 节点已被 GC 回收,跳过
    }
  }
}

当 DOM 节点从文档中移除且没有其他强引用时,WeakRef 不会阻止 GC 回收它。

但是,虽然WeakRef有避免DOM内存泄露的能力,但我个人觉得不推荐这么使用,原因后面会解释。

3. 事件监听 + 不阻止 GC

有内存问题的代码示意:

// ❌ 泄漏:闭包持有 DOM 引用,且监听器未清理
function setup() {
  const el = document.getElementById('target');
  window.addEventListener('resize', () => {
    el.style.width = '100%'; // el 被闭包捕获,永远无法回收
  });
}

如果使用WeakRef:

function setup() {
  const ref = new WeakRef(document.getElementById('target'));
  window.addEventListener('resize', () => {
    const el = ref.deref();
    if (!el) return; // 对象没了,直接跳过
    el.style.width = '100%';
  });
}

还是那句话,这里其实使用AbortController主动移出监听器是更好的做法。

4. 组件实例池 / 对象池(复用但不泄漏)

比方说下面这段代码,对象销毁后自动从池子里 “消失”,不用手动清理。

const pool = new Set();

function addToPool(obj) {
  pool.add(new WeakRef(obj));
}

function foreachActive(cb) {
  for (const ref of pool) {
    const obj = ref.deref();
    if (obj) cb(obj);
    else pool.delete(ref);
  }
}

5. 防止循环引用导致的内存泄漏

这个案例中父 ↔ 子互相引用,但是由于使用了WeakRef,打破了强引用链,GC 能正常工作,内存不会泄露。

class Parent {
  constructor() {
    this.child = null;
  }
}

class Child {
  constructor(parent) {
    this.parentRef = new WeakRef(parent); // 弱引用
  }
}

const p = new Parent();
const c = new Child(p);
p.child = c;

OK,虽然上面展示了很多可以使用WeakRef的场景,但是这些处理手段都不推荐(除了第一个场景),只能作为避免内存泄漏的最后手段。

三、谨慎使用WeakRef

为什么要谨慎使用WeakRef?最重要的原因其实也就四个字——“不可预测”!

垃圾收集何时、如何以及是否发生,取决于任何给定JavaScript引擎的实现。你在一个引擎中观察到的任何行为,在另一个引擎中、在同一引擎的另一个版本中,甚至在相同引擎的相同版本但在稍有不同的情况下,都可能会有所不同。

比方说某个内存,你明明想要使用,但是却被回收了,这就很烦人。

我之前开发就遇到过,大数据量的视频buffer数据,最终视频合成的时候莫名丢失了,使得我不得不一开始的时候强引用,当时我也是没有搞懂浏览器引擎的回收机制,根据查阅资料的说法,就是内存不足的时候,某些未被强引用的巨大变量会被回收,即使这个变量最后需要使用。

又比如说,你一位内存已经被清理了,可实际上内存清理工作可能会比预期晚得多才进行,或者根本不会进行。

因为JavaScript 引擎的内存回收机制极为复杂:

  • 即使两个对象同时变得无法访问(例如,由于分代收集),一个对象可能比另一个对象更早地被垃圾收集。
  • 垃圾收集工作可以通过使用增量和并发技术来分时段进行。
  • 可以使用各种运行时启发式算法来平衡内存使用和响应性。
  • JavaScript 引擎可能会保留对看似无法访问的内容的引用(例如,在闭包或内联缓存中)。
  • 不同的JavaScript引擎处理这些事情的方式可能不同,或者同一个引擎在不同版本中可能会改变其算法。
  • 复杂的因素可能导致对象被意外地保留很长时间,例如与某些应用程序编程接口(API)一起使用时。

所以,在绝大多数场景下,主动清理引用(移除监听器、清空变量、useEffect return cleanup)才是正道。

WeakRef 的 deref() 返回 undefined 的时机依赖 GC,不可预测,所以不应该用它来做确定性的资源管理。

例如上面案例提到的事件管理,下面的AbortController方法是更加推荐的:

function setup() {
  const el = document.getElementById('target');
  const controller = new AbortController();
  
  window.addEventListener('resize', () => {
    el.style.width = '100%';
  }, { signal: controller.signal });
  
  // 需要清理时
  controller.abort(); // 自动移除监听器
}

我个人的观点是这样的:你该怎么开发就怎么开发,不用管什么内存没有回收这些,大多数场景下,你多使用点内存,少一点内存,对用户影响几乎没有,让JS引擎自己去维护吧,除非遇到内存占有很大,不得不去处理的场景,这个时候,大家再去考虑使用WeakRef做内存优化。

四、结语碎碎念

我的个人博客开始出现很多AI味十足的评论,先是一句话总结文章内容,然后对内容和作者无脑吹,什么“硬核”,什么“高大上”。

还不如以前回复的“写的狗屁不通”这样的评论。[叹气]

最近刷漫剧,发现下面也有很多这样的评论,都是AI机器人。

其实龙虾火了之后,这样的东西会越来越普遍。

比方说即有同事弄了自动去社交网站评论的龙虾。

不过我目前还未深入这类AI工具,不急不急,对我而言,掌握更基础的知识与技能,比追逐流行工具更有价值。

好了,就说这么多吧,感谢大家的阅读,我们下周再见!

飞吻再见

😉😊😇
🥰😍😘

(本篇完)

分享到:


发表评论(目前没有评论)