美文网首页
Set 和 Map 数据结构

Set 和 Map 数据结构

作者: 了凡和纤风 | 来源:发表于2019-10-09 20:46 被阅读0次

    本文介绍 Set、WeakSet、Map、WeakMap 的基本用法

    一、Set

    1.1、基本用法

    ES6 提供了 新的数据结构 —— Set。它类似于数组,但是成员的值都是唯一的,没有重复。Set 本身是一个构造函数,用来生成 Set 数据结构

    const s = new Set();
    
    [2, 3, 5, 5, 6, 4, 4, 8].forEach(x => s.add(x))
    
    for (let i of s) {
      console.log(i)
    }
    // 235648
    

    上面 通过 add 方法向 Set 结构加入成员,结果表明 Set结构不会添加重复的值

    Set 函数可以接受一个数组(或者具有 iterator 接口的其他数据结构)作为参数,用来初始化。

    const set = new Set([1, 2, 3, 4, 4])
    [...set]
    // [1, 2, 3, 4]
    
    function divs() {
      return [...document.querySelectorAll('div')]
    }
    
    const set = new Set(divs())
    // 元素去重
    set.size
    

    数组去重

    [...new Set(array)]
    

    1.2、Set实例的属性和方法

    Set 结构的实例有以下属性。

    • Set.prototype.constructor:构造函数,默认就是Set函数
    • Set.prototype.size:返回 Set 实例的成员总数

    Set 的操作方法:

    • add(value):添加某个值,返回Set结构本身
    • delete(value):删除某个值,返回一个布尔值,表示是否删除成功
    • has(value):返回一个布尔值,表示参数是否为 Set 的成员
    • clear():清除所有成员,没有返回值

    Array .form 方法可以将 Set 结构转为数组

    const items = new Set([1, 2, 3, 4, 4])
    const array = Array.from(items)
    array // [1, 2, 3, 4]
    

    这就提供了一种去除数组的重复元素的方法

    function dedupe(array) {
      return Array.from(new Set(array))
    }
    
    dedupe([1, 1, 2, 3]) // [1, 2, 3]
    

    1.3、遍历操作

    Set 结构的实例有 4个遍历方法,可用于遍历成员

    • keys():返回键名的遍历器
    • values():返回键值的遍历器
    • entries():返回键值对的遍历器
    • forEach():使用回调函数遍历每个成员

    Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set保存一个回调函数列表,调用时就能保住按照添加顺序调用。

    keys()、values()、entries()

    keys 方法、values 方法、entries 方法都返回的是遍历器对象。由于Set 结构没有键名,只有键值(键名 和 键值 是同一个值),所以 keys 方法 和 values 方法的行为完全一致。

    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']
    

    entries 方法返回的遍历器同时包括键名 和 键值,所以每次输出一个数组,其两个成员完全相等。


    Set 结构的实例默认可遍历,其默认遍历器生成函数就是它的 values方法

    Set.prototype[Symbol.iterator] === Set.prototype.values
    

    这意味着,可以省略 values 方法,直接用 for...of 循环遍历 Set

    forEach()

    Set 结构实例的 forEach 方法用于对每个成员执行某种操作,没有返回值

    let set = new Set([1, 2, 3])
    
    set.forEach((value, key) => console.log(value * 2))
    // 2
    // 4
    // 6
    

    forEach 还有第二个参数,表示绑定的 this 对象

    遍历的应用
    扩展运算符(...) 内部使用 for...of 循环,所以也可以用于 Set 结构

    let set = new Set(['red', 'green', 'blue'])
    let arr = [...set]
    // ['red', 'green', 'blue']
    

    数组的 map 和 filter 方法也可以用于 set

    let set = new Set([1, 2, 3,])
    set = new Set([...set].map(x => x * 2))
    // Set(3) {2, 4, 6}
    
    let set = new Set([1, 2, 3, 4, 5])
    set = new Set([...set].filter(x =>( x % 2 ) == 0))
    // Set(2) {2, 4}
    

    因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect) 和 差集(Difference)

    let a = new Set([1, 2, 3])
    let b = new Set([4, 3, 2])
    
    // 并集
    let union = new Set([...a, ...b])
    //Set(4) {1, 2, 3, 4}
    
    // 交集
    let intersect = new Set([...a].filter(x => b.has(x)))
    // Set(2) {2, 3}
    
    // 差集
    let difference = new Set([...a].filter(x => !b.has(x)))
    // Set(1) {1}
    

    如果想在遍历操作中改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种时利用原Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构:另一种是利用 Array.from 方法。

    // 方法一
    let set = new Set([1, 2, 3])
    set = new Set([...set].map(val => val * 2))
    // {2, 4, 6}
    
    // 方法二
    let set = new Set([1, 2, 3])
    set = new Set(Array.from(set, val => val * 2))
    // {2, 4, 6}
    

    二、WeakSet

    2.1、含义

    WeakSet 结构与 Set 类似,也是步重复的值的集合。但是,它与 Set 有两个区别:

    1. WeakSet 的成员只能是对象,而不能是其他类型的值。
    2. WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用。(也就是说,如果其他对象都不在引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象是否还存在与 WeakSet 之中)

    2.2、语法

    WeakSet 是一个构造函数,可以使用 new 命令创建 WeakSet 数据结构。

    const ws = new WeakSet()
    

    WeakSet 可以接受一个数组或类似数组的对象作为参数。实际上,任何具有 iterable 接口的对象都可以作为 WeakSet 的参数。

    const a = [[1, 2], [3, 4]]
    const ws = new WeakSet(a)
    
    // WeakSet {[1, 2], [3, 4]}
    

    成为 WeakSet 的成员的是 a 数组的成员,而不是 a 数组本身。这意味着,数组的成员只能是对象。

    WeakSet 结构有以下 3 个方法

    • WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。
    • WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员
    • WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例中。
    const ws = new WeakSet()
    const obj = {}
    const foo = {}
    
    ws.add(window)
    ws.add(obj)
    
    ws.has(window) // true
    ws.has(foo) // false
    
    ws.delete(window)
    ws.has(window)// false
    

    WeakSet 没有 size 属性,没有办法遍历成员

    WeakSet 不能遍历,因为成员都是弱引用,随时可能消失,遍历机制无法保证成员存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处是存储 DOM 节点,而不用担心这些节点从文档移除时会引发内存泄漏。

    const foos = new WeakSet()
    
    class Foo {
      constructor() {
        foos.add(this)
      }
    
      method() {
        if (!foos.has(this)) {
          throw new TypeError('Foo.prototype.method 只能在 Foo 实例上调用')
        }
      }
    }
    

    上面的代码保证了 Foo 的实例方法只能在 Foo 的实例上调用。这里使用 WeakSet的好处是,数组 foos 对实例的引用不会被计入回收机制,所以删除实例的时候不用考虑 foos,也不会出现内存泄漏。

    三、Map

    3.1、含义和基本用法

    JavaScript 的对象(Object) 本质上是键值对的集合(Hash 结构),但是只能用字符串作为键。这给它的使用带来了很大的限制。为了解决这个问题,ES6提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象) 都可以作为键。(也就是说,Object 结构提供了 “字符串——值” 的对应,Map 结构提供了“值——值”的对应,是一种更完善的 Hash 结构实现)

    const m = new Map()
    const o = {p: 'Hello World'}
    
    m.set(o, 'content')
    m.get(o) // 'content'
    
    m.has(o) // true
    m.delete(0) // true
    m.has(0) // false
    

    Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组

    const map = new Map([
      ['name', '张三'],
      ['title', 'Author']
    ])
    
    map.size // 2
    map.has('name') // true
    map.get('name') // "张三"
    map.has('title') // true
    map.get('title') // "Author"
    

    Map 构造函数姐搜数座作为参数,实际上执行的是下面的算法

    const items = [
      ['name', '张三'],
      ['title', 'Author']
    ]
    
    const map = new Map()
    
    items.forEach(
      ([key, value]) => map.set(key, value)
    )
    

    事实上,任何具有 Iterator 接口且每个成员都是一个双元素数组的数据结构都可以当作 Map 构造函数的参数。也就是说,Set 和 Map 都可以用来生成新的 Map。

    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
    

    注意事项

    • 如果对同一个键多次赋值,后面的值将覆盖前面的值
    • 如果读取一个未知的键,则返回 undefined
    • Map 的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。

    只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心

    const map = new Map()
    
    map.set(['a'], 555)
    map.get(['a']) // undefined
    
    • 同理,同样的值的两个实例在 Map 结构中被视为两个键
    • 如果 Map 的键是一个简单类型的值,则只要两只严格相等,Map就将其视为一个键,包括 0 -0。另外,虽然 NaN 不严格等于自身,但是 Map 将其视为同一个键。

    3.2、实例的属性和操作方法

    Map 结构的实例有以下几个属性和操作方法

    • size:返回Map 结构的成员总数
    • set(key, value):设置 key 所对应的键值,然后返回整个Map 结构。如果 key 已经有值,则键值会被更新,否则就生成该键。set方法返回的是当前的 Map 对象,因此可以采用链式写法。
    • get(key):get 方法读取 key 对应的键值,如果找不到 key,则返回 undefined
    • has(key):has 方法返回一个布尔值,表示某个键是否在 Map 数据结构中。
    • delete(key): delete 方法删除某个键,返回 true。如果删除失败,则返回false
    • clear():清除所以成员,没有返回值

    3.3、遍历方法

    Map 原生提供了 3个 遍历器生成函数和一个遍历方法。

    • keys():返回键名的遍历器
    • values():返回键值的遍历器
    • entries():返回所有成员的遍历器
    • forEach():遍历 Map 的所有成员

    需要特别注意的是,Map 的遍历顺序就是插入顺序。

    const map = new Map([
      ['F', 'no'],
      ['T', 'yes']
    ])
    
    for (let key of map.keys()) {
      console.log(key)
    }
    //"F"
    // "T"
    
    for (let value of map.values()) {
      console.log(value)
    }
    // "no"
    // "yes"
    
    for (let item of map.entries()) {
      console.log(item[0], item[1])
    }
    // "F" "no"
    // "T" "yes"
    
    // 或者
    
    for( let [key, value] of map.entries()) {
      console.log(key, value)
    }
    // "F" "no"
    // "T" "yes"
    

    Map 结构的默认遍历器接口(Symbol.interator 属性)就是 entries 方法

    map[Symbol.iterator] === map.entries
    // true
    
    for(let [key, value] of map) {}
    // 等同于
    for (let [key, value] of map.entries()) {}
    

    Map 结构转为数组结构的比较快速的方法是结合扩展运算符(...)

    const map = new Map([
      [1, 'one'],
      [2, 'two'],
      [3, 'three']
    ])
    
    [...map.keys()];
    // [ 1, 2, 3]
    
    [...map.values()];
    // ["one", "two", "three"]
    
    [...map.entries()];
    // [[1, "one"],  [2, "two"], [3, "three"]]
    
    [...map]
    // [[1, "one"],  [2, "two"], [3, "three"]]
    

    结合数组的 map 方法、filter 方法,可以实现 Map 的遍历和过滤(Map 本身没有 map 和 filter 方法)

    const map0 = new Map()
      .set(1, 'a')
      .set(2, 'b')
      .set(3, 'c')
    
    const map1 = new Map(
      [...map0].map(([k, v]) => [k * 2, '_' + v])
    )
    map1
    // Map(3) {2 => "_a", 4 => "_b", 6 => "_c"}
    
    
    const map2 = new Map(
      [...map0].filter(([k, v]) => k < 3)
    )
    // Map(2) {1 => "a", 2 => "b"}
    

    此外,Map 还有一个 forEach 方法,与数组的 forEach 方法类似,也可以实现遍历。forEach 方法还可以接受第二个参数,用于绑定 this

    const reporter = {
      report: function(key, value) {
        console.log("key: %s, Value: %s", key, value)
      }
    }
    
    map.forEach(function(value, key, map) {
      this.report(key, value)
    }, reporter)
    

    3.4、与其他数据结构的互相转换

    Map 转为数组
    使用扩展运算符(...)

    const myMap = new Map()
      .set(true, 7)
      .set(1, 'b');
    
    [...myMap]
    

    数组转为 Map
    将数组传入 Map 构造函数就可以转为 Map

    new Map([
      [true, 7],
    ])
    

    Map 转为对象
    如果 Map 的所有键都是字符串,则可以转为对象

    function strMapToObj(strMap) {
      let obj = Object.create(null)
      for (let [k, v] of strMap) {
        obj[k] = v
      }
      return obj
    }
    

    对象转为Map

    function objToStrMap(obj) {
      let strMap = new Map()
      for (let k of Object.kets(obj)) {
        strMap.set(k, obj[k])
      }
      return strMap
    }
    

    Map 转为 JSON
    Map 转为 JSON 要区分两种情况:

    1. Map 的建名都是字符串,这是可以选择转为对象JSON
    function strMapToJson(strMap) }
      return JSON.stringify(strMapToObj(strMap))
    }
    
    1. Map 的键名有非字符串,这时可以选择转为数组JSON
    function mapToArrayJson(map) {
      return JSON.stringify([...map])
    }
    

    JSON 转为 Map
    JSON转为 Map,正常情况下所有键名都是字符串。

    function jsonToStrMap(jsonStr) {
      return objToStrMap(JSON.parse(jsonStr))
    }
    

    有一种特殊情况:整个JSON就是一个数组,且每个数组成员本身又是一个具有两个成员的数组。这是,它可以一一对应地转为Map。这往往时数组转为JSON的逆操作。

    function jsonToMap(jsonStr) {
      return new Map(JSON.parse(jsonStr))
    }
    

    四、WeakMap

    4.1、含义

    WeakMap 结构与 Map 结构类似,也用于生成键值对的集合。

    • WeakMap 可以使用 set 方法添加成员
    • WeakMap 也可以接受一个数组,作为构造函数的参数

    WeakMap 和 Map 的区别有以下两点:

    1. WeakMap 只接受对象作为键名(null 除外),不接受其他类型的值作为键名
    const map = new WeakMap()
    map.set(1, 2)
    // TypeError: Invalid value used as weak map key
    
    1. WeakMap 的键名所指向的对象不计入垃圾回收机制。

    WeakMap 的设计目的在于,又是我们想在某个对象上面存放一些数据,但是这会形成对这个对象的引用

    如下例子

    const e1 = document.getElementById('foo')
    const e2 = document.getElementById('bar')
    const arr = [
      [e1, 'foo 元素'],
      [e2, 'bar 元素']
    ]
    

    如上,e1 和 e2 是两个对象,我们通过 arr 数组对这两个对象添加一些文字说明,这就形成了 arr 对 e1 和 e2 的引用。

    一旦不再需要这两个对象,我们必须手动删除这个引用,否则垃圾回收机制就不会释放 e1 和 e2 占用的内存。

    // 手动删除引用
    arr[0] = null
    arr[1] = null
    

    上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄漏

    WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用,典型的应用场景是,在网页的 DOM 元素上添加数据时就可以使用 WeakMap 结构。当该DOM 元素被清除,其所对应的 WeakMap 记录就会自动被移除
    总之,WeakMap的专用场景就是它的键所对应的对象可能会在将来消失的场景,WeakMap 结构有助于防止内存泄漏。

    const wm = new WeakMap()
    
    let key = {}
    let obj = {foo: 1}
    
    wm.set(key, obj)
    obj = null
    wm.get(key)
    // Object {foo: 1}
    

    上面的代码中,键值 obj 是正常引用的,所以,即使在 WeakMap 外部消除了 obj 的引用,WeakMap 内部的引用依然存在

    注意:WeakMap 弱引用的只是键名而不是键值。键值依然是正常引用的。

    4.2、WeakMap 的语法

    WeakMap 与 Map 在 API 上的区别主要有两个:

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

    4.3、WeakMap 示例

    通过 Node 的 process.memoryUsage 方法看出来

    首先,打开 Node 命令行。 --expose-gc 参数表示允许手动执行垃圾回收机制

    $ node --expose-gc
    

    然后执行下面代码
    手动执行一次垃圾回收,保证获取的内存使用状态准确

    > global.gc()
    undefined
    

    查看内存占用的初始状态,heapUsed 为 4.6MB 左右

    > process.memoryUsage()
    { rss: 22802432,
      heapTotal: 7585792,
      heapUsed: 4720600,
      external: 8713 }
    
    > let wm = new WeakMap()
    undefined
    
    // 新建一个变量key,指向一个 5 * 1024 * 1024 的数组
    > let key = new Array(5 * 1024 * 1024)
    undefined
    
    // 设置 WeakMap 实例的键名,也指向 key 数组
    // 这时,key 数组的引用计数为 2
    // 变量 key 引用一次,WeakMap 的键名引用第二次
    > wm.set(key, 1)
    WeakMap{}
    
    > global.gc()
    undefined
    

    这是内存占用 heapUsed 增加到了(这里应该会增加)

    > process.memoryUsage()
    { rss: 22802432,
      heapTotal: 7585792,
      heapUsed: xxxxxxx,
      external: 8713 }
    
    // 然后清除 变量 key 对数组的引用
    > key = null
    
    // 再次再次执行垃圾回收
    > global.gc()
    
    // 再次查看
    > process.memoryUsage()
    
    

    4.4、WeakMap 用途

    1. WeakMap 应用的典型场景就是以 DOM 节点作为键名的场景。

    let myElement = document.getElementById('logo')
    let myWeakmap = new WeakMap()
    
    myWeakmap.set(myElement, {timesClicked: 0})
    
    myElement.addEventListener('click', function() {
      let logoData = myWeakmap.get(myElement)
      logData.timesClicked++
    })
    

    每当发生 click 事件就更新一下状态。一旦这个 DOM 节点删除,改状态就会自动消失,不存在内存泄漏风险。

    2. 注册监听事件的 listener 对象很适合用WeakMap 来实现

    const listener = new WeakMap()
    
    listener.set(element1, handler1)
    listener.set(element2, handler2)
    
    element1.addEventListener('click', listener.get(element1), false)
    element2.addEventListener('click', listener.get(element2), false)
    

    监听函数放在 WeakMap 里面。一旦 DOM 对象消失,与它绑定的监听函数也会自动消失。

    3. WeakMap 的另一个用处时部署私有属性

    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
    

    上面的代码中,Countdown 类的两个内部属性 —— _counter 和 _action —— 是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄漏

    相关文章

      网友评论

          本文标题:Set 和 Map 数据结构

          本文链接:https://www.haomeiwen.com/subject/virwpctx.html