Skip to content

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 结构加入成员,不会添加重复的值。

规则

  1. 加入值不会发生类型转换
  2. 內部值判断,类似 ===,但 认为 NaN 等于 NaN
js
let set = new Set()
let a = NaN
let b = NaN
set.add(a)
set.add(b)
set // Set {NaN}
  1. 空对象不等
js
let set = new Set()

set.add({})
set.size // 1

set.add({})
set.size // 2

应用:去重

js
[...new Set(array)] // 数组去重
[...new Set(string)].join('') // 字符串去重

遍历规则

  1. 遍历顺序为插入顺序
  2. 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"]
  1. Set 结构实例默认可遍历,可以省略valuesfor...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)
}
  1. 通过扩展运算符,可将数组方法间接用于 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 结构

  1. 利用 Set 结构映射,赋值给原来的 Set
  2. 利用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 区别

  1. 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 set
  1. WeakSet中对象为弱引用,其他对象不引用该对象,自动回收,不考虑是否还在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数据结构:键值对集合,各种类型都能为键

  1. 接收数组作为参数
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))
  1. 任何具有 iterator 接口、且每个成员是双元素的数组的数据解构都可以作为参数

SetMap都可以用来生成新的 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
  1. 同一键多次赋值,后面值覆盖前面值
js
const map = new Map()

map.set(1, 'aaa').set(1, 'bbb')

map.get(1) // "bbb"
  1. 未知键,返回 undefined
js
new Map().get('asfddfsasadf')
// undefined
  1. 对同一对象的引用,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
  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])
}

结构转换

  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'}
  1. 数组转 Map

数组传入 Map 构造函数

js
new Map([
  [true, 7],
  [{ foo: 3 }, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }
  1. 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 }
  1. 对象转 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}
  1. 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"]]]'
  1. 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 区别

  1. 只接受对象作为键名(除 null,其余均报错)
  2. 弱引用,该对象其他引用清除,自动回收
  3. 没有遍历操作
  4. 无法清空

下面代码,需要手动删除引用,否则不会自动释放内存

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 语法

  1. 没有遍历操作。没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。没有size属性、keys()values()entries()方法
  2. 无法清空,不支持clear
  3. 只有 get()set()has()delete()方法

WeakMap 示例

process.memoryUsage:查看内存

shell
node --expose-gc
js
// 手动执行一次垃圾回收,保证获取的内存使用状态准确
> 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 用途

  1. 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
)
  1. 部署私有属性

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()
// DONE

WeakRef

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(),输出一条错误信息。

相关链接

[-] Set 和 Map