js 中的 Set 与 WeakSet 以及弱引用相关知识

ES6 之前,我们如果想保存数据,一般都是放到数组或对象中。ES6 开始,新增了 Set、WeakSet 与 Map、WeakMap 这 4 种可以存储数据的数据结构。本篇文章就先来介绍下 Set 与 WeakSet。

Set

Set 对象是值的集合,并且这些值是不会重复的,之前写过一篇文章介绍了什么是集合,并基于对象实现了简单的 Set 。

创建

使用 Set 需要先通过 new 创建出一个 Set 对象:

javascript 复制代码
// 例 1.1
const set = new Set()

方法和属性

  • 方法

往 Set 对象添加元素使用的是 add 方法,比如我们往例 1.1 创建的 Set 对象中添加一个数字 1:

javascript 复制代码
// 例 1.2
set.add(1)

因为 Set 中的元素只会出现一次,所以如果我们继续添加一个 1,set 中依旧只有一个 1。但是,如果我们添加的不是基本数据类型,而是引用类型的,比如一个对象:

javascript 复制代码
// 例 1.2.1
set.add({})
set.add({})

那么添加两次,在 set 中会有两个 {},因为在内存中这两个空对象的地址是不一样的。而如果是下面这种情况,那么 Set 中将只有一个空对象,原因也很简单 ------ 它们在内存中的地址是一样的:

javascript 复制代码
// 例 1.2.2
const obj = {}
set.add(obj)
set.add(obj)

除了 add 方法,Set 还有 clear()delete()entries()has()keys()values() 等实例方法,都比较简单,基本看名字就知道是干啥用的,具体可以直接参看 MDN 文档,不再赘述。如果要遍历 Set 对象,可以使用 forEach()for..of

  • 属性

Set 有一个实例属性 size,返回 Set 对象中元素的个数:

javascript 复制代码
// 例 1.2.3
const arr = [1, 2, 3, 1, 4, 5, 1]
const set = new Set(arr)
console.log(set.size) // 5

注意这里我们是直接在 new Set() 的时候,传入可迭代对象 arr 作为参数,该对象中所有迭代值都会被加入到生成的 set 中

应用

在一些涉及到数组去重的面试题里,用 Set 能够快速的实现效果(至于真实项目可以使用 lodash):

javascript 复制代码
// 例 1.3
const arr = [1, 2, 3, 1, 4, 5, 1]
const set = new Set(arr)
const newArr = [...set]

例 1.3 中,先在第 2 行直接传入 arr 生成 Set 对象 set,然后我们再利用展开语法通过 set 构造出不带重复元素的数组 newArr,因为 set 也是可迭代对象,满足数组中使用展开语法的要求。

WeakSet

WeakSet 是一种与 Set 类似的数据结构,内部存储的元素也不能重复。其与 Set 的区别主要有 2 点:

  1. WeakSet 只能存储对象类型的数据,而 Set 则既可以存储基本数据类型,又可以存储对象;
  2. WeakSet 对存储的对象都是弱引用 ,意思是如果某个对象没有在其它地方被引用,就会被 GC 处理回收。但是存储在 Set 中的对象都是强引用,只要从 GC Roots (根节点,可以看成是 VE)出发能找到 Set 对象,那么存储在其中的对象就不会被当成垃圾回收。

弱引用

这里对弱引用相关的知识做点补充。在例 2.1 中,我们将对象 { name: 'Jay' } 的引用 obj1 直接赋值给另一个变量 obj2,那么即使在后面我们将 obj1 指向了 null,打印 obj2.name 得到的依然是 Jay。因为 obj1 和 obj2 对于 { name: 'Jay' } 的引用都是强引用,只要二者存其一,那么内存中的 { name: 'Jay' } 对象就不会被 GC 回收:

javascript 复制代码
// 例 2.1
let obj1 = { name: 'Jay' }
const obj2 = obj1
obj1 = null
console.log(obj2.name) // Jay

WeakRef

在 ES12(ES2021)中,新增了 WeakRef 对象,它包含了对对象的弱引用。使用方式如下:

javascript 复制代码
// 例 2.2
let obj1 = { name: 'Jay' }
const obj2 = new WeakRef(obj1)

想获取 WeakRef 对象所绑定的 target 对象的属性,比如例 2.2 中的 obj2.name,是无法直接打印获取的,需要借助 WeakRef 的实例方法 deref() 先获取到 { name: 'Jay' },再去获取属性:

javascript 复制代码
// 例 2.2.1
console.log(obj2.deref().name) // Jay

如此,obj2 对 { name: 'Jay' } 的引用就为弱引用,如果我们在之后也将 obj1 赋值为了 null,那么内存中 { name: 'Jay' } 对象就会在某个时刻被 GC 回收,之后再想查看 obj2 的 name 属性,得到的会是 undefined。 注意:WeakRef 与下面介绍的 FinalizationRegistry 相关内容比较新,所以只有比较新的版本的浏览器或 Node.js 才支持。

FinalizationRegistry

想要验证的话可以利用同为 ES12 的新对象 FinalizationRegistry ------ 可以让我们在某个对象被垃圾回收时请求一个回调:

javascript 复制代码
// 例 2.2.2
let obj1 = { name: 'Jay' }
const obj2 = new WeakRef(obj1)
const registry = new FinalizationRegistry(heldValue => {
  console.log(heldValue + '被销毁了') // obj1被销毁了
  console.log(obj2.deref()?.name) // undefined
})
registry.register(obj1, 'obj1')
obj1 = null

例 2.2.2 中,我们通过 new FinalizationRegistry() 的方式构建了一个 FinalizationRegistry 对象 registry,在构建时可以传入一个回调函数 ------ 当在注册表中注册的对象被回收时 ------ 就会被调用。第 8 行代码所做的就是通过 FinalizationRegistry 对象的实例方法 register() 往注册表中注册对象 obj1。注册表中是可以注册多个对象的,为了方便知道是哪个对象被销毁从而触发了回调,可以在 register() 传入第二个参数('obj1')作为标识,该值可以在回调中获取(heldValue)。如此,当第 9 行 obj1 指向 null 后,由于 obj2 的引用是个弱引用,所以{ name: 'Jay' } 对象会被回收,obj2.deref() 为 undefined。因为第 6 行用到了 ES11 的 ?. 可选链操作符,故而打印结果为表达式 obj2.deref()?.name 的短路返回值 undefined。

方法与遍历

WeakSet 没有 size 属性,实例方法也没有 Set 那么多,只有 add()delete()has()。而且由于 WeakSet 只是对对象的弱引用,所以不能使用 forEach()for..of 遍历获取其中的对象,以免这些对象不能正常的销毁。比如打印查看 WeakSet 对象会发现结果为 { <items unknown> },因为打印需要先遍历:

javascript 复制代码
// 例 2.2.3
let obj1 = { name: 'Jay' }
const ws = new WeakSet()
ws.add(obj1)
console.log(ws) // WeakSet { <items unknown> }

应用

平常开发中没用到过 WeakSet,所以它的应用场景我就举个案例来说明。比如我们有个 Singer 类,并且有个实例方法 sing,如果我们只想让 sing 是被 Singer 的实例对象直接调用,也就是保证 this 的指向只能是 Singer 的实例,而无法通过诸如 apply、call 或 bind 之类的方法改变,那么就可以如下例 2.3 那样,在 constructor 方法中将 this 保存到一个 WeakSet 对象中,然后在调用 sing 时进行判断:

javascript 复制代码
// 例 2.3
const ws = new WeakSet()
class Singer {
  constructor() {
    ws.add(this)
  }
  sing() {
    if (!ws.has(this)) {
      throw new Error('this 必须指向 Singer 实例')
    }
    console.log('是谁在唱歌', this)
  }
}
const singer = new Singer()
singer.sing.apply() // Error: this 必须指向 Singer 实例

这里使用 WeakSet 的好处在于添加到其中的对象都是弱引用,那么如果后续代码中将 singer 赋值为 null 了,GC 就可以正确的将其清除。

相关推荐
_.Switch15 分钟前
Python Web 架构设计与性能优化
开发语言·前端·数据库·后端·python·架构·log4j
libai18 分钟前
STM32 USB HOST CDC 驱动CH340
java·前端·stm32
南斯拉夫的铁托1 小时前
(PySpark)RDD实验实战——取最大数出现的次数
java·javascript·spark
Java搬砖组长1 小时前
html外部链接css怎么引用
前端
GoppViper1 小时前
uniapp js修改数组某个下标以外的所有值
开发语言·前端·javascript·前端框架·uni-app·前端开发
丶白泽1 小时前
重修设计模式-结构型-适配器模式
前端·设计模式·适配器模式
程序员小羊!1 小时前
UI自动化测试(python)Web端4.0
前端·python·ui
破z晓1 小时前
OpenLayers 开源的Web GIS引擎 - 地图初始化
前端·开源
好看资源平台1 小时前
JavaScript 数据可视化:前端开发的核心工具
开发语言·javascript·信息可视化
维生素C++1 小时前
【可变模板参数】
linux·服务器·c语言·前端·数据结构·c++·算法