js 中的 Map与 WeakMap

上篇文章我们介绍了 js 中的 Set 与 WeakSet,本篇继续介绍同样是 es6 新增的 Map 与 WeakMap。

Map

Map 对象用于存储映射关系,也就是一些键值对。我们知道,对象也可以用来存储键值对,但是对象中,键(属性名)的类型只能是 String 或 Symbol,而 Map 中键的类型可以是任何值(对象或者原始值)。如果在对象中,也使用 ES6 对于对象字面量进行的增强写法 ------ 计算属性名,让对象作为属性名,如下例 3.1:

javascript 复制代码
// 例 3.1
const a = { a: 'a' }
const b = { b: 'b' }
const obj = {
  [a]: 'A',
  [b]: 'B'
}
console.log(obj) // { '[object Object]': 'B' }

我们会发现第 8 行打印 obj 得到的对象中只有一个键值对。原因在于以对象作为属性名时,对象 a 和 b 都会被转换为字符串 [object Object],所以属性名相同,后一个覆盖了前一个。

创建和添加元素

Map 对象的创建方式依旧是通过 new 创建,然后可以通过 set() 方法往里面添加元素:

javascript 复制代码
// 例 3.2
const a = { a: 'a' }
const b = { b: 'b' }
const map = new Map()
map.set(a, 'A')
map.set(b, 'B')
map.set('name', 'Jay')
console.log(map) // Map { { a: 'a' } => 'A', { b: 'b' } => 'B', 'name' => 'Jay' }

除了通过 set() 一个个添加,也可以在新建 Map 对象时,直接传入个数组,只不过这个数组是个二维数组,里面这个数组固定只有 2 个元素,前一个为 Map 中元素的键,后一个为值(类似 ts 中的元组):

javascript 复制代码
// 例 3.2.1
// ... 对象 a 和 b 的定义,同例 3.2
const map = new Map([
  [a, 'A'],
  [b, 'B'],
  ['name', 'Jay']
])

Map 对象也有 size 属性,实例方法中除了 set() 之外还有 get()has() 等,遍历也可以用 forEach() 以及 for..of,都比较简单,可以直接查阅 MDN 文档,此处不再多费笔墨。如果我们在浏览器里打印查看例 3.2 或例 3.2.1 的结果,均如下图所示:

可以看到 Map 对象里有个叫 [[Entries]] 的属性,下面介绍 2 个与之有关的方法。

Object.entries()

ES8 中新增的 Object.entries() 的返回值就是像例 3.2.1 中 [[a, 'A'], [b, 'B'], ['name', 'Jay']] 这样的二维数组,里面的数组由一个给定对象自身可枚举属性的键值对组成:

javascript 复制代码
// 例 3.3
const obj = {
  name: 'Jay',
  age: 40
}
console.log(Object.entries(obj)) // [ [ 'name', 'Jay' ], [ 'age', 40 ] ]

也可以往 Object.entries() 中传入数组,返回得到的每个小数组里第 1 个元素为对应值在传入的数组中的索引值:

javascript 复制代码
// 例 3.3.1
const arr = ['Monday', 'Tuesday', 'Wednesday']
console.log(Object.entries(arr)) // [ [ '0', 'Monday' ], [ '1', 'Tuesday' ], [ '2', 'Wednesday' ] ]

Object.fromEntries()

Object.entries() 是由对象得到键值对数组,如果我们想从键值对列表转为对象呢?则可以使用 ES10 新增的 Object.fromEntries()

javascript 复制代码
// 例 3.4
const obj1 = { a: 'a', b: 'b' }
const entry = Object.entries(obj1)
console.log(Object.fromEntries(entry)) // { a: 'a', b: 'b' }

有个应用场景是可以结合 URLSearchParams 接口将 URL 的查询字符串转为对象:

javascript 复制代码
// 例 3.4.1
const paramsString = 'id=rs123456&type=hat'
const searchParams = new URLSearchParams(paramsString)
const paramsObject = Object.fromEntries(searchParams)
console.log(paramsObject) // { id: 'rs123456', type: 'hat' }

将查询字符串直接传给 URLSearchParams() 构造函数,得到的是一个可迭代的对象,在例 3.4.1 中如果打印 searchParams 得到的将是 { 'id' => 'rs123456', 'type' => 'hat' },将它传给 Object.fromEntries() 即可得到对象形式的查询数据。

WeakMap

WeakMap 是与 Map 类型相似的一种数据结构,也是以键值对的形式存在,区别有 2 点(和 Set 与 WeakSet 的区别异曲同工):

  1. WeakMap 存储的键值对中的键,只能是对象(ES14 开始 Symbol 也可以);
  2. WeakMap 中作为键的对象的引用,是弱引用,而 Map 中是强引用。

下面举个例子进行证明:

javascript 复制代码
// 例 4
let obj1 = { a: 'a' }
let obj2 = { b: 'b' }
const map = new Map()
const wm = new WeakMap()
map.set(obj1, 'A')
wm.set(obj2, 'B')
const registry = new FinalizationRegistry(heldValue => {
  console.log(heldValue + '被销毁了')
  console.log(wm)
})
registry.register(obj1, 'obj1')
registry.register(obj2, 'obj2')
obj1 = null
obj2 = null
console.log(map)

我们创建了 obj1 和 obj2 两个对象,然后分别在第 14、15 行赋值为 null,因为 Map 中作为键的 obj1 是强引用,所以 obj1 不会被 GC 销毁,而 WeakMap 中作为键的 obj2 是弱引用,会在 obj2 变为 null 后的某个时间被 GC 销毁,所以最终的打印结果如下图:

WeakMap 没有 size 属性,也不可枚举,不能使用 forEach()for..of,实例方法则有 set()get()has()delete(),具体参见 MDN 文档。

应用

vue3 的响应式原理,就有用到 WeakMap。在 vue3 项目中,从我们改变数据到页面视图的渲染更新,中间过程比较复杂,涉及到诸如虚拟 dom,diff 算法,依赖收集等,但其实从原理上看,可以简化成如下例 5 那样,改变某个对象的某个属性,会触发一些函数的执行。比如我们改变了 obj1 和 obj2 中的 name 属性,就需要立马触发一些相应的函数 ------ 对应 obj1.nameobj1NameFirstStep()obj1NameSecondStep() 和对应 obj2.nameobj2NameFirstStep()------ 帮助我们最终将视图更新,而 vue3 在处理属性和函数的映射关系时,就借助了 WeakMap 以及 Map:

javascript 复制代码
// 例 5
const obj1 = { name: 'Jay' }
const obj2 = { name: 'Zhou'}

function obj1NameFirstStep() {
  console.log('处理 obj1 的 name 改变时要执行的相关方法(第一步)')
}
function obj1NameSecondStep() {
  console.log('处理 obj1 的 name 改变时要执行的相关方法(第二步)')
}

function obj2NameFirstStep() {
  console.log('处理 obj2 的 name 改变时要执行的相关方法')
}

const obj1Map = new Map()
obj1Map.set('name', [obj1NameFirstStep, obj1NameSecondStep])

const obj2Map = new Map()
obj2Map.set('name', [obj2NameFirstStep])

const wm = new WeakMap()
wm.set(obj1, obj1Map)
wm.set(obj2, obj2Map)

// 改变 obj1 和 obj2 的 name 属性
wm.get(obj1).get('name').forEach(item => item())
wm.get(obj2).get('name').forEach(item => item())

在第 16 行和第 19 行,分别新建了 Map 对象,将 obj1 和 obj2 的各个属性(为了简化例 5 中 2 个对象均只有 1 个属性 name)和对应的函数存储其中,因为 WeakMap 的键只能是对象,而这里需要是字符串,所以此处用的是 Map。在第 22 行新建了 WeakMap 对象,并将各个对象及存储了对应的属性和函数的关系的 Map 对象存入,如此,一旦我们监听到 obj1 或 obj2 的某个属性发生了改变,就可以用 get()方法,先通过对象找到对应的 Map,再通过对应的属性名找到相关的函数数组,最后遍历执行即可。22 行这里之所以用 WeakMap 而不是 Map,则是因为避免如果之后我们将 obj1 或 obj2 赋值为了 null,GC 无法正确回收销毁相关对象。

相关推荐
码爸8 分钟前
flink doris批量sink
java·前端·flink
深情废杨杨9 分钟前
前端vue-父传子
前端·javascript·vue.js
J不A秃V头A1 小时前
Vue3:编写一个插件(进阶)
前端·vue.js
司篂篂2 小时前
axios二次封装
前端·javascript·vue.js
姚*鸿的博客2 小时前
pinia在vue3中的使用
前端·javascript·vue.js
宇文仲竹2 小时前
edge 插件 iframe 读取
前端·edge
Kika写代码2 小时前
【基于轻量型架构的WEB开发】【章节作业】
前端·oracle·架构
天下无贼!4 小时前
2024年最新版Vue3学习笔记
前端·vue.js·笔记·学习·vue
Jiaberrr4 小时前
JS实现树形结构数据中特定节点及其子节点显示属性设置的技巧(可用于树形节点过滤筛选)
前端·javascript·tree·树形·过滤筛选
赵啸林4 小时前
npm发布插件超级简单版
前端·npm·node.js