Map、Set与Symbol

Map、Set 与 Symbol

前言

这几个数据结构是 ES6 新增的,还记得当时第一次面试被问到“你对这几个了解多少?有使用过吗?”时,啥也不会特尴尬 🫢,所以现在详细了解并整理一下这几个数据结构。

Map

Map 是什么?让我们来看一下 MDN 文档的介绍:

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为键或值。

可以看到其中几个关键点:保存键值对、记住键的原始插入顺序、任何值都能作为键或值

这些就是他的特点,相较于 object 不同的地方,不过其实他和 object 也有很多共同之处,而且很多时候用哪个都可以。

基本使用

// 创建一个空映射
const map1 = new Map()

// 使用嵌套数组初始化映射
const map2 = new Map([
['key1', 'val1'],
['key2', 'val2'],
])

初始化之后可以通过set()来添加键值对,此外还可以通过get()has()来查询,可以用size属性来获取数量,还可以用delete()clear()来删除值

const m = new Map()

m.set('first', 1)
console.log(m.get('first')) // 1

m.set('second', 2)
console.log(m.has('second')) // true
console.log(m.size) // 2

m.delete('first')
console.log(m.size) // 1

m.clear()
console.log(m.size) // 0

由于set()方法返回的是映射实例,所以也可以初始化的时候连起来操作

const m = new Map().set('key1', 'val1')
m.set('key2', 2)

键的唯一性

Map中,一个键只能出现一次,其中键的比较基于SameValueZero (零值相等)算法,基本上是相当于使用严格对象相等的标准来检测。

SameValueZero 是 ES 规范内部定义,语言中不能使用,JS 提供的判断只有=====Object.is()

const m = new Map()

const functionKey = function () {}
const fKey2 = function () {}

m.set(functionKey, 'functionVal')

console.log(m.get(functionKey)) // functionVal
console.log(m.get(fKey2)) // undefined

与严格相等一样,在映射中用作键和值的对象,在自己的内容或属性被修改是仍然保持不变

const m = new Map()

const objKey = {}
const objVal = {}

m.set(objKey, objVal)
console.log(m.get(objKey)) // {}
objKey.key = 'key'
objVal.val = 'val'
console.log(m.get(objKey)) // { val: 'val' }

不过这个相等算法,也可能会导致意想不到的冲突:

const m = new Map()

const a = NaN,
b = NaN,
c = +0,
d = -0

console.log(a === b) // false
console.log(c === d) // true

m.set(a, 'NaN value')
m.set(c, '+0 value')

console.log(m.get(b)) // NaN value
console.log(m.get(d)) // +0 value

顺序与迭代

MapObject的一个主要差异就是,Map会维护插入顺序,因此可以根据插入顺序来执行迭代操作。它提供了一个迭代器,可以通过entries()或者Symbol.iterator属性来获取

const m = new Map([
['key1', 1],
['key2', 2],
])

console.log(m.entries === m[Symbol.iterator]) // true
const iterator = m.entries()

for (const [key, val] of iterator) {
console.log(key, val)
// key1 1
// key2 2
}

因为entries()是默认迭代器,所以可以直接使用拓展操作把映射转成数组

const m = new Map([
['key1', 1],
['key2', 2],
])

console.log([...m]) // [ [ 'key1', 1 ], [ 'key2', 2 ] ]

也可以使用映射的forEach来迭代

const m = new Map([
['key1', 1],
['key2', 2],
])

m.forEach((value, key) => {
console.log(key, value)
// key1 1
// key2 2
})

通过keys()可以获取映射的键迭代器,values()可以获得值的迭代器。

对比 Object

他们俩比较相似,而且用法也差不多,日常开发中用哪个基本相差不大,不过还是有一些优劣的。

下面的对比摘录自《JavaScript 高级程序设计(第 4 版)》

  • 内存占用:Map 占用更少
  • 插入性能:Map 性能更佳
  • 查找速度:Object 更好
  • 删除性能:Map 更好

关键词:迭代器、SameValueZero 算法、性能、Object

WeakMap

看名字就知道他和Map关系很大,看看 MDN 介绍先:

WeakMap 是一种键值对的集合,其中的键必须是对象或非全局注册的符号,且值可以是任意的 JavaScript 类型,并且不会创建对它的键的强引用。换句话说,一个对象作为 WeakMap 的键存在,不会阻止该对象被垃圾回收。一旦一个对象作为键被回收,那么在 WeakMap 中相应的值便成为了进行垃圾回收的候选对象,只要它们没有其他的引用存在。唯一可以作为 WeakMap 的键的类型是非全局注册的符号,因为非全局注册的符号是保证唯一的,并且不能被重新创建。

简单看就是,基本功能和Map一样,但是他的键必须是对象,原始数据类型作为键会报错

const sy = Symbol()
const wm = new WeakMap([
[sy, 1],
[2, 2],
])
// TypeError: Invalid value used as weak map key
console.log('🚀 ~ wm.get(sy):', wm.get(sy))

弱键

WeakMap中的weak表是键是弱弱的拿着,意思就是这些键不是正式引用,不会阻止垃圾回收。

看一个例子:

const wm = new WeakMap()
wm.set({}, 'val')

set()初始化了一个新的空对象并作为一个字符串的键。因为没有指向这个对象的其他引用,所以这行代码执行之后,这个对象键会被当作垃圾回收掉。然后这个键值对就从弱映射中消失了,变成了一个空映射。

另一个例子:

const wm = new WeakMap()
const container = {
key: {},
}

wm.set(container.key, 'value')
console.log(wm.get(container.key)) // value

container.key = null
console.log(wm.get(container.key)) // undefined

在这个例子里面,container维护了一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标,但是当container.key = null调用时,会清理掉。

同时也因为弱引用,所以他不可迭代,同时也没有clear()方法

用处

因为他不会妨碍垃圾回收,所以说他非常适合保存关联元数据

const m = new Map()
const btn = document.querySelector('#login')
m.set(btn, 'val')

假设这个代码执行后,那个按钮从 DOM 树中删除掉了。但是 Map 还保存着引用,所以对应的 DOM 节点还会逗留在内存中。

如果是使用的弱映射,当节点被删除后,垃圾回收程序就可以立即释放内存。

const wm = new WeakMap()
const btn = document.querySelector('#login')
wm.set(btn, 'val')

关键词:不可迭代、垃圾回收

Set

我一直理解的 set 就挺像数组的,但是里面都是唯一值,看看 MDN 咋说:

Set 对象允许你存储任何类型(无论是原始值还是对象引用)的唯一值。

Set 对象是值的合集(collection)。集合(set)中的元素只会出现一次,即集合中的元素是唯一的。你可以按照插入顺序迭代集合中的元素。插入顺序对应于 add() 方法成功将每一个元素插入到集合中(即,调用 add() 方法时集合中不存在相同的元素)的顺序。

他的 api 也和 Map 比较类似,而且他的值相等算法也是SameValueZero(零值相等)算法

基本使用

const s = new Set(['val1', 'val2'])
console.log(s.size) // 2

初始化后可以使用add()添加值,使用has()查询,使用size获取数量,以及可以用delete()clear()删除元素。

顺序和迭代

Map类似,Set也提供了迭代器,可以通过values()keys()来获取,他俩是一样的,Symbol.iterator属性引用的是values()

const s = new Set([1, '2'])

console.log(s.values === s[Symbol.iterator]) // true
console.log(s.keys === s[Symbol.iterator]) // true
console.log(s.values === s.keys) // true

for (const val of s.values()) {
console.log(val) // 1 2
}

如果是用entries()或者forEach则是俩个重复的值:

const s = new Set([1, '2'])

for (const pairs of s.entries()) {
console.log(pairs)
// [ 1, 1 ]
// [ '2', '2' ]
}

s.forEach((val, dupVal) => {
console.log(val, dupVal)
// 1 1
// 2 2
})

用处

一般用的多的就是数组去重吧

const unique = arr => [...new Set(arr)]

WeakSet

WeakSet 是可被垃圾回收的值的集合,包括对象和非全局注册的符号WeakSet 中的值只能出现一次。它在 WeakSet 的集合中是唯一的。

它和 Set 对象的主要区别有:

  • WeakSet 只能是对象和符号的集合,它不能像 Set 那样包含任何类型的任意值。
  • WeakSet弱引用WeakSet 中对象的引用为引用。如果没有其他的对 WeakSet 中对象的引用存在,那么这些对象会被垃圾回收。

WeakSet就和WeakMap类似

作用

递归调用自身的函数需要一种通过跟踪哪些对象已被处理,来应对循环数据结构的方法。

// 对传入的 subject 对象内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
// 避免无限递归
if (_refs.has(subject)) {
return
}
fn(subject)
if (typeof subject === 'object') {
_refs.add(subject)
for (const key in subject) {
execRecursively(fn, subject[key], _refs)
}
}
}

const foo = {
foo: 'Foo',
bar: {
bar: 'Bar',
},
}

foo.bar.baz = foo // 循环引用!
function fn(obj) {
return console.log(obj)
}
execRecursively(fn, foo)

Symbol

Symbol 是一种基本数据类型,每一个从Symbol()获取的 symbol 值都是唯一的,一个 symbol 值可以作为对象属性的标识符,这是该数据类型仅有的目的。

symbol 是一种基本数据类型(primitive data type)。Symbol() 函数会返回 symbol 类型的值,该类型具有静态属性和静态方法。它的静态属性会暴露几个内建的成员对象;它的静态方法会暴露全局的 symbol 注册,且类似于内建对象类,但作为构造函数来说它并不完整,因为它不支持语法:”new Symbol()“。

Symbol('11') === Symbol('11') // false

他不会被常规的枚举方法包含,如for...inObject.keys()

const age = Symbol('age')
const obj = {
name: 'aaa',
[age]: 18,
}

console.log(obj[age]) // 18
console.log(obj.age) // undefined

for (const key in obj) {
console.log(key) // name
}

console.log(Object.keys(obj)) // [ 'name' ]
console.log(Object.getOwnPropertyNames(obj)) // [ 'name' ]