WeakMap、Map还傻傻分不清?WeakMap为何能被GC?

在此之前我们先看看什么是GC(浏览器的垃圾回收机制),内存泄漏 想必大家都知道它,它会给我们项目的性能带来不小的危害。

程序运行中会有一些垃圾数据不再使用,需要及时释放出去,如果我们没有及时释放,这就是内存泄露 。然而GC负责自动管理内存,确保不再使用的对象能够被及时清理,防止内存泄漏和内存溢出,从而保证浏览器的稳定运行和用户体验的流畅。

浏览器的垃圾回收过程

浏览器的垃圾回收过程通常包括以下几个步骤:

  1. 标记阶段:垃圾回收器从根(roots)开始,递归访问对象的属性,将访问过的对象都标记为"活动"的。根通常是全局对象,或者是执行上下文栈中的变量和函数。
  2. 清除阶段:在标记阶段完成后,垃圾回收器会遍历堆中的所有对象,将那些没有被标记为"活动"的对象识别为垃圾,并释放它们占用的内存。

浏览器的垃圾回收算法

浏览器通常使用以下几种垃圾回收算法:

  1. 引用计数算法 :每个对象都有一个引用计数器,当有一个地方引用它时,计数器加1 ;当引用被删除或超出作用域时,计数器减1 。当计数器为0时,对象就被视为垃圾。但这种算法存在循环引用的问题,因此现代浏览器已不再使用。
  2. 标记-清除算法 :这是目前浏览器中最常用的垃圾回收算法。它分为标记和清除两个阶段,如前所述。标记-清除算法可以处理循环引用问题,但可能会导致内存碎片化
  3. 标记-整理算法:这种算法在标记-清除的基础上,将所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。这样可以解决内存碎片化的问题,但效率相对较低。

标记回收

在我们开发过程中,如果想让GC回收一个对象,我们可以这样做:

ini 复制代码
let a = {}

a = null

我们可以给引用的值设置设置为null,这时这个对象就失去了所有对它的引用,从而变得不可达。GC在运行时,会检测到这些不可达的对象,并将它们标记为垃圾,进而在后续的某个时刻释放它们占用的内存。

但是如果一个对象被多次引用时,例如作为另一对象的键、值或子元素时,将该对象引用设置为null,该对象不会被回收。

ini 复制代码
let a = {}
let b = {}

b = [a]
a = null
console.log(b) // [{}]

那如果Map的键呢?

WeakMap VS Map

先说说他们的区别:

  1. 类型:Map的键可以是任何类型的值,包括基本类型(如字符串或数字)和对象引用。而WeakMap的键只能是对象类型。这是因为WeakMap的设计初衷是存储对象及其对应的值,并且其键对对象的引用是 的,不会阻止GC回收键所引用的对象。
  2. 引用关系:Map中的键值对是强引用关系,只要Map对象存在,其中的键值对就不会被自动回收。相反,WeakMap中的键值对是弱引用关系。如果WeakMap中使用的某个键对象没有被其他地方引用,那么在垃圾回收时,这个键对象及其对应的值都会被自动回收。
  3. 可枚举性与大小:Map支持对键和值进行迭代,可以使用size属性获取键值对的数量,也可以使用clear方法清空Map。然而,WeakMap不支持对键和值进行迭代,也没有size属性和clear方法,无法获取WeakMap的所有键或值。

下面是一个简单的例子来说明这两者之间的区别:

csharp 复制代码
// 创建一个Map对象  
let map = new Map();  
  
// 创建一个对象作为键  
let key = {};  
  
// 在Map中存储键值对  
map.set(key, 'value');  
  
// 此时,即使没有其他地方引用key对象,它也不会被垃圾回收,因为Map仍然强引用着它  
  
// 创建一个WeakMap对象  
let weakMap = new WeakMap();  
  
// 使用相同的对象作为键  
weakMap.set(key, 'weak value');  
  
// 假设我们现在删除了所有对key对象的引用(除了在WeakMap中的引用)  
key = null;  
  
// 在下一次垃圾回收时,key对象会被回收,因为它在WeakMap中的引用是"弱"的  
// 因此,与key对象相关联的值也会从WeakMap中消失

这个例子展示了WeakMapMap在处理键对象引用时的不同行为。在Map中,即使没有其他地方引用键对象,它也不会被垃圾回收,因为Map强引用着它。而在WeakMap中,如果键对象没有其他强引用,它会在垃圾回收时被回收,同时与之相关联的值也会从WeakMap中消失。

WeakMap 回收过程

  1. process.memoryUsage(): 是 Node.js 中的一个方法,用于获取 Node.js 进程的内存使用情况。它返回一个对象,描述了 Node.js 进程的内存使用情况,单位是字节(bytes)。
  2. global.gc():配合 --expose-gc 实现手动GC
javascript 复制代码
const toMb = num => num / 1024 / 1024 + 'MB'

if (global.gc === undefined) {  
  console.error('GC is 不支持. 请使用--expose-gc启动node.');  
  process.exit(1); // 退出进程
} 

const weakMap = new WeakMap()

let arr = new Array(100 * 10000)

weakMap.set(arr, 1)

// 打印当前的内存使用情况  
console.log('当前内存使用情况:', toMb(process.memoryUsage().heapTotal))

// 移除对arr的所有引用,除了 WeakMap 中的那个
arr = null

// 请求垃圾回收  
global.gc()

// 等待一段时间,让垃圾回收器有机会运行  
setTimeout(() => {  
  // 再次打印内存使用情况  
  console.log('GC之后的内存使用情况:', toMb(process.memoryUsage().heapTotal))
  
  // 尝试从 WeakMap 中获取之前存储的值  
  // 如果 arr 已经被垃圾回收,这里将返回 undefined  
  const value = weakMap.get(arr) 
  console.log('WeakMap 中的值:', value) // 应该输出 undefined  
}, 100) // 等待 100 毫秒以确保垃圾回收器有时间运行

由此可见如果没被GC造成的内存泄露是一个很严重的问题,利用WeakMap解决这种问题就会简单很多。

实际应用场景

  1. WeakMap : 在开发中我们可能需要将某些数据与DOM元素关联起来。使用WeakMap,我们可以将DOM元素作为键,相关的数据作为值。这样,当DOM元素被从DOM树中移除时,由于WeakMap的弱引用特性,与该元素关联的数据也会自动被垃圾回收,无需手动清理,从而避免了内存泄漏。
dart 复制代码
const root = document.querySelector('.root')
const map = new WeakMap()
map.set(root, '啦啦啦')

// 删除root节点
root.remove()
// 弱引用特性
map.get(root) // undefined

由于 root 元素已经被垃圾回收,WeakMap 中不再有这个键的条目,所以返回的结果是 undefined

  1. MapMap则是一个更通用的键值对集合,其键可以是任何类型(不仅仅是对象或字符串)。这使得Map在许多场景中都非常有用。

利用Map实现计数器:

csharp 复制代码
const str = 'hello world';  
const charCountMap = new Map();  
  
// 遍历字符串中的每个字符  
for (let char of str) {  
  // 如果字符已经在 Map 中,则增加计数  
  if (charCountMap.has(char)) {  
    charCountMap.set(char, charCountMap.get(char) + 1);  
  } else {  
    // 如果字符不在 Map 中,则添加到 Map 并设置计数为 1  
    charCountMap.set(char, 1);  
  }  
}  

或许会有人问MapObject有什么区别,虽然都可以用来存储键值对,但它们之间存在一些重要的差异,例如键值类型、键的顺序、是否可迭代、size大小之列的。

MapWeakMap在不同场景下具有各自的优势和适用性。在选择使用哪种数据结构时,应根据具体需求来选择。

相关推荐
疯狂的沙粒1 分钟前
如何在Vue项目中应用TypeScript?应该注意那些点?
前端·vue.js·typescript
小镇程序员17 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐19 分钟前
前端图像处理(一)
前端
程序猿阿伟26 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒28 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪37 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背40 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M1 小时前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc1 小时前
学习electron
javascript·学习·electron
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js