Weakmap应该怎么用?

103 阅读6分钟

1:前言

说到 WeakMap 是 JavaScript 中的一种数据结构,由ES6规范提供,具有独特的特性和用途。 虽然名字让人觉得它只是一个更弱的 Map,但实际上 WeakMap 提供了一种独特的内存管理方式和数据存储机制,能够在特定场景下显著提高性能和简化代码。 本文我们将深入探讨 WeakMap 的特性、应用场景以及它如何改变我们处理数据的方式。

2:什么是 WeakMap?

WeakMap 是一种集合,它必须以对象或者是symbol作为键,而与之关联的值则是“弱引用”。 这意味着如果一个对象没有其他引用指向它,JavaScript 的垃圾回收机制可以自动回收这个对象,从而避免内存泄漏。

2.1:关键特性

  • 键的弱引用:WeakMap 的键必须是对象,而值可以是任何类型。这种弱引用的特性让它在内存管理上非常高效(这种结构非常有意思,在VUE的框架源码中有大量使用案例非常微妙)。
  • 不可枚举:与 Map 不同,WeakMap 的键值对不能被枚举。这使得它更适合存储私有数据,避免数据被意外访问或修改(私有化)。
  • 自动垃圾回收:当一个对象作为键的引用被删除时,WeakMap 会自动清理与之关联的值,这对于管理动态数据尤其重要(这可能是最重要的特性)。

2.2:应用场景

1. 缓存和性能优化

在我们常见的应用中,缓存是提高性能的关键。使用 WeakMap 可以有效地缓存数据,而不会导致内存泄漏。 例如:当我们需要缓存某些计算结果时,可以使用 WeakMap 来存储这些结果,确保在不再使用时能够被垃圾回收。

const cache = new WeakMap();

const expensiveComputation = (obj) => {
  if (cache.has(obj)) {
      return cache.get(obj);
  }
  
  const result = /* 复杂计算 */;
  cache.set(obj, result);
  return result;
}

// 在这个例子中,cache 只会在 obj 被其他引用解除后才被清理,从而实现高效的内存管理。

2. 私有属性

在 JavaScript 中,虽然可以使用闭包来创建私有属性,但这种方式有时会显得繁琐。WeakMap 可以作为一种简洁的解决方案,将私有数据与对象关联。

const privateData = new WeakMap()

class User {
  constructor(name) {
    privateData.set(this, { name })
  }

  getName() {
    return privateData.get(this).name
  }
}

// 上面代码中,User 类的内部属性 privateData 是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

3. DOM 节点的元数据

在处理 DOM 节点时,通常需要为节点附加一些元数据。使用 WeakMap 可以将这些元数据与节点关联,而不会影响节点的生命周期。

const elementData = new WeakMap();

const setElementData(element, data) => {
    elementData.set(element, data);
}

const getElementData(element) => {
    return elementData.get(element);
}
// 使用示例
const div = document.createElement('div');
setElementData(div, { info: 'This is a div' });
console.log(getElementData(div)); // { info: 'This is a div' }

// 在这个场景中,WeakMap 确保了元数据与 DOM 节点之间的关联是安全的,且不会导致内存泄漏。

3:Map 与 WeakMap 同以对象为键为啥 WeakMap 可以被回收?

确实这是一个很有趣的问题,强引用(strong reference)和弱引用(weak reference)到底是啥?

3.1:垃圾回收机制

JavaScript 的垃圾回收机制通常采用标记-清除算法(Mark-and-Sweep) 。以下是该算法的基本工作原理:

  1. 标记阶段:从根对象(如全局对象和当前执行堆栈中的变量)开始,递归地标记所有可达的对象。
  2. 清除阶段:遍历堆中的所有对象,回收那些没有被标记的对象。

3.2:强引用的实现

在标记-清除算法(注意这个东西并没有你想的那么统一)中,强引用的实现通常如下:

  1. 引用计数:每个对象都有一个计数器,记录有多少个强引用指向它。当引用增加时,计数器增加;当引用减少时,计数器减少。如果计数器为零,则对象可以被垃圾回收。
  2. 标记阶段:从根对象开始,递归标记所有强引用的对象。这些对象被认为是"可达的"。
  3. 清除阶段:回收所有未被标记的对象。

3.3:弱引用的实现

弱引用的实现更加复杂一些,因为它允许对象在没有强引用时被回收。以下是弱引用在 WeakMap 和 WeakSet 中的实现原理:

  1. 弱引用存储WeakMap 和 WeakSet 的键使用弱引用存储。这意味着这些键不会增加对象的引用计数。
  2. 垃圾回收:在标记阶段,垃圾回收器不会考虑弱引用的对象。如果一个对象只有弱引用而没有强引用,它将被标记为不可达,并在清除阶段被回收。
  3. 自动清理:当垃圾回收器回收一个对象时,WeakMap 和 WeakSet 会自动移除对该对象的引用。这是一种隐式行为,开发者无法直接检测或枚举这些引用。

3.4:细节

虽然 JavaScript 规范没有详细描述这些数据结构的内部实现,但我们可以推测一些实现细节:

  1. 引用追踪
    • 强引用:引擎维护一个对象图,图的节点是对象,边是强引用。在标记阶段,从根节点开始递归遍历整个图。
    • 弱引用:弱引用不会在对象图中创建强连接。引擎可能使用一个特殊的弱引用表来追踪这些引用,但在标记阶段不会遍历它们。
  2. 自动清理
    • 强引用:在清除阶段,回收所有未被标记的对象。
    • 弱引用:在清除阶段,检查弱引用表,移除所有指向已被回收对象的引用。

4:结论

WeakMap 是一个强大的API,能够在内存管理、数据隐私和性能优化等方面提供显著优势。它的弱引用特性和不可枚举性使得它在处理动态数据时更高效更安全。

WeakMap 作为 ES6 标准的一部分,值得我们深入探索和应用。最后希望上文中提及的一些简单实践能给你的编码带来一些灵感。

5:参考

es6.ruanyifeng.com/#docs/set-m…

juejin.cn/post/735908…

developer.mozilla.org/en-US/docs/…

6:好文推荐

  1. WEB极致性能优化指南
  2. 最佳实践 Monorepo + Pnpm + Vue3 + Element-Plus 0-1 完整教程
  3. Vite + Rollup项目如何大幅提升性能
  4. 面试官系列:请说说你对深拷贝、浅拷贝的理解
  5. 面试官系列:请你说说原型、原型链相关
  6. 面试官系类:请手写instanceof
  7. 10分钟快速手写实现:call/apply
  8. 面试官系列:请手写防抖或节流函数debounce/throttle