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 就可以正确的将其清除。

相关推荐
一斤代码2 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子2 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子2 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
coderlin_3 小时前
BI布局拖拽 (1) 深入react-gird-layout源码
android·javascript·react.js
伍哥的传说4 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409194 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding4 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js