Appearance
Set 和 Map
Set
Set 基本用法
类似数组,成员值唯一,没有重复值
Set本身是一个构造函数,用来生成 Set 数据结构。
参数:数组 或 具有 iterable 接口的其他数据结构
js
// 例一
const set = new Set([1, 2, 3, 4, 4])
;[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5])
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'))
set.size // 56
// 类似于
const set = new Set()
document.querySelectorAll('div').forEach((div) => set.add(div))
set.size // 56实例属性和方法
Set.prototype.constructor:构造函数,默认就是Set函数
Set.prototype.size:返回Set实例的成员总数
Set 实例方法分为:操作方法(用于操作数据)和 遍历方法(用于遍历成员)
| 操作方法 | 说明 |
|---|---|
| add(value) | 添加某个值,返回 Set 本身 |
| delete(value) | 删除某个值,返回布尔值 |
| has(value) | 是否有该值,返回布尔值 |
| clear | 清除所有成员 |
| 遍历方法 | |
| keys | 键名遍历 |
| values | 键值遍历 |
| entries | 键值对遍历 |
| forEach | 回调函数遍历 |
均为原型上方法
Set.prototype.keys()
add:向 Set 结构加入成员,不会添加重复的值。
规则
- 加入值不会发生类型转换
- 內部值判断,类似 ===,但 认为 NaN 等于 NaN
js
let set = new Set()
let a = NaN
let b = NaN
set.add(a)
set.add(b)
set // Set {NaN}- 空对象不等
js
let set = new Set()
set.add({})
set.size // 1
set.add({})
set.size // 2应用:去重
js
[...new Set(array)] // 数组去重
[...new Set(string)].join('') // 字符串去重遍历规则
- 遍历顺序为插入顺序
- Set 没有键名,keys 和 values 行为一致
js
let set = new Set(['red', 'green', 'blue'])
for (let item of set.keys()) {
console.log(item)
}
// red
// green
// blue
for (let item of set.values()) {
console.log(item)
}
// red
// green
// blue
for (let item of set.entries()) {
console.log(item)
}
// ["red", "red"]
// ["green", "green"]
// ["blue", "blue"]- Set 结构实例默认可遍历,可以省略
values用for...of遍历
js
Set.prototype[Symbol.iterator] === Set.prototype.values // true
for (let item of set.values()) {
console.log(item)
}
// values 可省略
for (let item of set) {
console.log(item)
}- 通过扩展运算符,可将数组方法间接用于 set
js
let set = new Set([1, 2, 3])
set = new Set([...set].map((x) => x * 2))
let a = new Set([1, 2, 3])
let b = new Set([4, 3, 2])
let union = new Set([...a, ...b]) // 并集
let intersect = new Set([...a].filter((x) => b.has(x))) // 交集
let difference = new Set([...a].filter((x) => !b.has(x))) // 差集遍历中同步改变 Set 结构
- 利用 Set 结构映射,赋值给原来的 Set
- 利用
Array.form
js
// 1
let set = new Set([1, 2, 3])
set = new Set([...set].map((val) => val * 2))
// 2
let set = new Set([1, 2, 3])
set = new Set(Array.from(set, (val) => val * 2))WeakSet
与 set 结构类似,不重复值的集合
与 Set 区别
WeakSet成员只能是对象
js
const ws = new WeakSet()
ws.add(1)
// TypeError: Invalid value used in weak set
ws.add(Symbol())
// TypeError: invalid value used in weak setWeakSet中对象为弱引用,其他对象不引用该对象,自动回收,不考虑是否还在WeakSet中。
根据垃圾回收机制,如果对象能被访问,就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。
WeakSet 里面的引用,都不计入垃圾回收机制,只要对象在外部消失,WeakSet 里面引用会自动消失。
由于随时会消失,垃圾回收机制何时运行不可预测,所以 WeakSet 不可遍历。
语法
WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。
参数:可迭代对象作为参数,该对象的所有迭代值都会被自动添加进生成的 WeakSet 对象。
null 被认为是 undefined
js
const a = [
[1, 2],
[3, 4]
]
const ws = new WeakSet(a)
// WeakSet {[1, 2], [3, 4]}数组成员为 WeakSet 成员,而不是数组本身
js
const b = [3, 4]
const ws = new WeakSet(b)
// Uncaught TypeError: Invalid value used in weak set(…)| 方法 | 说明 |
|---|---|
| add(value) | 向 WeakSet 实例添加一个新成员 |
| delete(value) | 清除 WeakSet 实例的指定成员 |
| has(value) | 某个值是否在 WeakSet 实例,返回布尔值 |
没有size属性,没有办法遍历它的成员。
js
ws.size // undefined
ws.forEach // undefined
ws.forEach(function (item) {
console.log('WeakSet has ' + item)
})
// TypeError: undefined is not a function应用:存储 DOM 节点,不担心节点从文档移除,引发内存泄漏
下面代码保证了Foo的实例方法,只能在Foo的实例上调用。
js
const foos = new WeakSet()
class Foo {
constructor() {
foos.add(this)
}
method() {
if (!foos.has(this)) {
throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!')
}
}
}使用 WeakSet 的好处:foos对实例的引用,不会被计入内存回收机制,所以删除实例,不用考虑foos,也不会出现内存泄漏。
Map
Map 基本使用
对象(Object):键值对的集合(Hash 结构)
限制:键名只能为 字符串
ES6 Map数据结构:键值对集合,各种类型都能为键
- 接收数组作为参数
js
const map = new Map([
['name', '张三'],
['title', 'Author']
])
// 等效
const items = [
['name', '张三'],
['title', 'Author']
]
const map = new Map()
items.forEach(([key, value]) => map.set(key, value))- 任何具有 iterator 接口、且每个成员是双元素的数组的数据解构都可以作为参数
Set和Map都可以用来生成新的 Map
js
const set = new Set([
['foo', 1],
['bar', 2]
])
const m1 = new Map(set)
m1.get('foo') // 1
const m2 = new Map([['baz', 3]])
const m3 = new Map(m2)
m3.get('baz') // 3- 同一键多次赋值,后面值覆盖前面值
js
const map = new Map()
map.set(1, 'aaa').set(1, 'bbb')
map.get(1) // "bbb"- 未知键,返回
undefined
js
new Map().get('asfddfsasadf')
// undefined- 对同一对象的引用,Map 结构才视其为同一键。map 键和内存地址绑定,内存地址不一样,视为两个键。
js
const map = new Map()
map.set(['a'], 555)
map.get(['a']) // undefined
const x = ['a']
map.set(x, 1)
map.get(x) // 1- 简单类型值,严格相等,就视为同一键。
特殊情况:NaN,不严格等于自身,但视为同一键
js
let map = new Map()
map.set(-0, 123)
map.get(+0) // 123
map.set(true, 1)
map.set('true', 2)
map.get(true) // 1
map.set(undefined, 3)
map.set(null, 4)
map.get(undefined) // 3
map.set(NaN, 123)
map.get(NaN) // 123实例属性和操作方法
size属性:返回 Map 结构的成员总数
| 操作方法 | |
|---|---|
set(key, value) | 设置键名key对应的键值为value,返回整个 Map |
get(key) | 读取key对应的键值。找不到,返回undefined |
has(key) | 表示某个键是否存在,返回布尔值 |
delete(key) | 删除某个键,返回布尔值 |
clear() | 清除所有成员 |
| 遍历方法 | |
keys() | 返回键名遍历器 |
values() | 返回键值的遍历器 |
entries() | 返回所有成员的遍历器 |
forEach() | 遍历所有成员 |
补充
set:返回整个 Map,可以进行链式调用。有值更新,无值添加。
Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。
js
map[Symbol.iterator] === map.entries // true
for (let [key, value] of map) {
console.log(key, value)
}
for (let item of map.entries()) {
console.log(item[0], item[1])
}结构转换
- Map 转数组
使用扩展运算符...
同 Set,可以结合数组方法。
js
const map0 = new Map().set(1, 'a').set(2, 'b').set(3, 'c')
const map1 = new Map([...map0].filter(([k, v]) => k < 3))
// Map 结构 {1 => 'a', 2 => 'b'}- 数组转 Map
数组传入 Map 构造函数
js
new Map([
[true, 7],
[{ foo: 3 }, ['abc']]
])
// Map {
// true => 7,
// Object {foo: 3} => ['abc']
// }- Map 转对象
遍历 map,手动赋值
Map 键为字符串,无损转对象,非字符串,会先转为字符串。
js
function strMapToObj(strMap) {
let obj = Object.create(null)
for (let [k, v] of strMap) {
obj[k] = v
}
return obj
}
const myMap = new Map().set('yes', true).set('no', false)
strMapToObj(myMap)
// { yes: true, no: false }- 对象转 Map
通过Object.entries()
js
let obj = { a: 1, b: 2 }
let map = new Map(Object.entries(obj))手动实现转换函数
js
function objToStrMap(obj) {
let strMap = new Map()
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k])
}
return strMap
}
objToStrMap({ yes: true, no: false })
// Map {"yes" => true, "no" => false}- Map 转 JSON
Map 键名都为字符串,转为对象 JSON
js
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap))
}
let myMap = new Map().set('yes', true).set('no', false)
strMapToJson(myMap)
// '{"yes":true,"no":false}'Map 键名有非字符串,转为数组 JSON
js
function mapToArrayJson(map) {
return JSON.stringify([...map])
}
let myMap = new Map().set(true, 7).set({ foo: 3 }, ['abc'])
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'- JSON 转 Map
对象 JSON
js
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr))
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}数组 JSON
js
function jsonToMap(jsonStr) {
return new Map(JSON.parse(jsonStr))
}
jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}WeakMap
含义
与Map结构类似,也是用于生成键值对的集合。
与 Map 区别
- 只接受对象作为键名(除 null,其余均报错)
- 弱引用,该对象其他引用清除,自动回收
- 没有遍历操作
- 无法清空
下面代码,需要手动删除引用,否则不会自动释放内存
js
const e1 = document.getElementById('foo')
const e2 = document.getElementById('bar')
const arr = [
[e1, 'foo 元素'],
[e2, 'bar 元素']
]
// 手动删除引用
arr[0] = null
arr[1] = null下面代码中,对 element 的引用为弱引用。dom 被清除,对应的 WeakMap 记录自动移除。
js
const wm = new WeakMap()
const element = document.getElementById('example')
wm.set(element, 'some information')
wm.get(element) // "some information"WeakMap的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。
弱引用的是键名,不是键值
js
const wm = new WeakMap()
let key = {}
let obj = { foo: 1 }
wm.set(key, obj)
obj = null
wm.get(key)
// Object {foo: 1}消除了 obj 的引用,但是 WeakMap 中引用依然存在
WeakMap 语法
- 没有遍历操作。没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。没有
size属性、keys()、values()和entries()方法 - 无法清空,不支持
clear - 只有
get()、set()、has()、delete()方法
WeakMap 示例
process.memoryUsage:查看内存
shell
node --expose-gcjs
// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> global.gc();
undefined
// 查看内存占用的初始状态,heapUsed 为 4M 左右
> process.memoryUsage();
{ rss: 21106688,
heapTotal: 7376896,
heapUsed: 4153936,
external: 9059 }
> let wm = new WeakMap();
undefined
// 新建一个变量 key,指向一个 5*1024*1024 的数组
> let key = new Array(5 * 1024 * 1024);
undefined
// 设置 WeakMap 实例的键名,也指向 key 数组
// 这时,key 数组实际被引用了两次,
// 变量 key 引用一次,WeakMap 的键名引用了第二次
// 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
> wm.set(key, 1);
WeakMap {}
> global.gc();
undefined
// 这时内存占用 heapUsed 增加到 45M 了
> process.memoryUsage();
{ rss: 67538944,
heapTotal: 7376896,
heapUsed: 45782816,
external: 8945 }
// 清除变量 key 对数组的引用,
// 但没有手动清除 WeakMap 实例的键名对数组的引用
> key = null;
null
// 再次执行垃圾回收
> global.gc();
undefined
// 内存占用 heapUsed 变回 4M 左右,
// 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
> process.memoryUsage();
{ rss: 20639744,
heapTotal: 8425472,
heapUsed: 3979792,
external: 8956 }由此可见,外部引用消失,WeakMap 内部引用会自动垃圾回收
Chrome 浏览器的 Dev Tools 的 Memory 面板,有一个垃圾桶的按钮,可以强制垃圾回收(garbage collect)
WeakMap 用途
- DOM 节点作为键名
dom 节点删除,会自动进行垃圾回收
js
let myWeakmap = new WeakMap()
myWeakmap.set(document.getElementById('logo'), { timesClicked: 0 })
document.getElementById('logo').addEventListener(
'click',
function () {
let logoData = myWeakmap.get(document.getElementById('logo'))
logoData.timesClicked++
},
false
)- 部署私有属性
Countdown类的两个内部属性_counter和_action,是实例的弱引用。删除实例,它们也就随之消失,不会造成内存泄漏
js
const _counter = new WeakMap()
const _action = new WeakMap()
class Countdown {
constructor(counter, action) {
_counter.set(this, counter)
_action.set(this, action)
}
dec() {
let counter = _counter.get(this)
if (counter < 1) return
counter--
_counter.set(this, counter)
if (counter === 0) {
_action.get(this)()
}
}
}
const c = new Countdown(2, () => console.log('DONE'))
c.dec()
c.dec()
// DONEWeakRef
WeakSet 和 WeakMap 是基于弱引用的数据结构。
ES2021 - WeakRef :直接创建对象的弱引用
js
let target = {}
let wr = new WeakRef(target)wr就是一个 WeakRef 的实例,属于对target的弱引用,垃圾回收机制不会计入这个引用,也就是说,wr的引用不会妨碍原始对象target被垃圾回收机制清除
deref():原始对象存在,该方法返回原始对象;如果原始对象被清除,该方法返回undefined。
js
let target = {}
let wr = new WeakRef(target)
let obj = wr.deref()
if (obj) {
// target 未被垃圾回收机制清除
// ...
}应用:缓存数据
js
function makeWeakCached(f) {
const cache = new Map()
return (key) => {
const ref = cache.get(key)
if (ref) {
const cached = ref.deref()
if (cached !== undefined) return cached
}
const fresh = f(key)
cache.set(key, new WeakRef(fresh))
return fresh
}
}
const getImageCached = makeWeakCached(getImage)标准规定,一旦使用WeakRef()创建了原始对象的弱引用,那么在本轮事件循环(event loop),原始对象肯定不会被清除,只会在后面的事件循环才会被清除
FinalizationRegistry
ES2021 - 引入了清理器注册表功能 FinalizationRegistry:指定目标对象被垃圾回收机制清除以后,所要执行的回调函数。
js
const registry = new FinalizationRegistry((heldValue) => {
// ....
})
registry.register(theObject, 'some value')theObject被垃圾回收后,将 “some value”作为参数,传入 FinalizationRegistry 中回调函数
取消已经注册的回调函数,使用 unregister。但需要向 register 传入第三参数
js
registry.register(theObject, 'some value', theObject)
registry.unregister(theObject)加强缓存函数
增加一个清理器注册表,一旦缓存的原始对象被垃圾回收机制清除,会自动执行一个回调函数。该回调函数会清除缓存里面已经失效的键。
js
function makeWeakCached(f) {
const cache = new Map()
const cleanup = new FinalizationRegistry((key) => {
const ref = cache.get(key)
if (ref && !ref.deref()) cache.delete(key)
})
return (key) => {
const ref = cache.get(key)
if (ref) {
const cached = ref.deref()
if (cached !== undefined) return cached
}
const fresh = f(key)
cache.set(key, new WeakRef(fresh))
cleanup.register(fresh, key)
return fresh
}
}
const getImageCached = makeWeakCached(getImage)另一个示例
js
class Thingy {
#file
#cleanup = (file) => {
console.error(`The \`release\` method was never called for the \`Thingy\` for the file "${file.name}"`)
}
#registry = new FinalizationRegistry(this.#cleanup)
constructor(filename) {
this.#file = File.open(filename)
this.#registry.register(this, this.#file, this.#file)
}
release() {
if (this.#file) {
this.#registry.unregister(this.#file)
File.close(this.#file)
this.#file = null
}
}
}上面示例中,如果由于某种原因,Thingy类的实例对象没有调用release()方法,就被垃圾回收机制清除了,那么清理器就会调用回调函数#cleanup(),输出一条错误信息。