• 【ECMAScript6】Set与Map


    一、集合Set

    ES6 提供了新的数据结构 Set(集合)。它类似于数组,但成员的值都是唯
    一的,集合实现了 iterator 接口,所以可以使用『扩展运算符』和『for…of…』进
    行遍历,集合的属性和方法:

    • size 返回集合的元素个数
    • add 增加一个新元素,返回当前集合
    • delete 删除元素,返回 boolean 值
    • has 检测集合中是否包含某个元素,返回 boolean 值
    • clear 清空集合,返回 undefined
    1.1 基本API
            let s = new Set()
            let s1 = new Set(['AAA', 'BBB', 'CCC', 'DDD'])
            console.log(s1.size)
            console.log(s1.add('XXX'))
            console.log(s1.delete('AAA'))
            console.log(s1.has('CCC'))
    
            s1.clear()
            console.log(s1)
    
            for(let v of s1) {
                console.log(v)
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    在这里插入图片描述

    1.2 集合的一些基本操作
            let arr = [1, 2, 3, 4, 5, 4, 3, 2, 1]
    
            // 1.数组去重
            let result = [...new Set(arr)]
            console.log(result)
    
            // 2. 求两个集合的交集
            let arr2 = [4, 5, 6, 5, 6]
            let result1 = [...new Set(arr)].filter(item => {
                let s2 = new Set(arr2)
                if(s2.has(item)){
                    return true 
                }else {
                    return false
                }
            })
            console.log(result1)
    
            let result2 = [...new Set(arr)].filter(item => {return new Set(arr2).has(item)})
            console.log(result2)
    
            // 3. 求两个集合的并集
            let union = [...new Set([...arr, ...arr2])]
            console.log(union)
    
            // 4. 求两个集合的差集
            // arr与arr2的差集:arr中有的元素而arr2中没有
            let diff1 = [...new Set(arr)].filter(item => !(new Set(arr2).has(item)))
            console.log(diff1)
            // arr2与arr的差集:arr2中有的元素而arr中没有
            let diff2 = [...new Set(arr2)].filter(item => !(new Set(arr).has(item)))
            console.log(diff2)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    在这里插入图片描述


    二、弱集合WeakSet

    2.1 基本API
    • WeakSet.prototype.add(value) :向 WeakSet 实例添加一个新成员。
    • WeakSet.prototype.delete(value) :清除 WeakSet 实例的指定成员。
    • WeakSet.prototype.has(value) :返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
    • WeakSet中的值任何时候都可能被销毁,所以没有必要提供迭代其值的能力,故没有迭代和clear的API。
    2.2 Set与WeakSet的区别

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

            const ws = new WeakSet()
            ws.add({})
            console.log(ws)
    
    • 1
    • 2
    • 3

    上面add()方法初始化一个新对象,并将它作为WeakSet的值。由于没有指向这个对象的其它引用,所以当这行代码执行完成后,这个对象值就会被当作垃圾回收。然后这个值就从弱集合中消失了,使其成为一个空集合。

            const ws = new WeakSet()
            const container = {
                val: {}
            }
            ws.add(container.val)
    
            function removeReference() {
                container.val = null
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    container对象维护着一个对弱集合值的引用,所以container.val不会作为垃圾回收的目标,但是如果调用了removeReference后就会摧毁值对象的最后一个引用,垃圾回收就可以把这个值清理掉。

    2.3 一个需要注意的‘bug’
            let obj = {name: 'Leozi'}
            let ws = new WeakSet()
            ws.add(obj)
            obj = null
            console.log(ws)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    执行上面代码后理论上的结果是ws是一个空的弱集合,但是控制台展示如下:
    在这里插入图片描述

    原因:因为浏览器的垃圾回收机制是不受我们控制的,我们无法知道垃圾回收机制什么时候执行,之所以打印能出现内容,其实是因为打印的时候垃圾回收机制没有启动。所以我们可以认为,上图二中的打印在垃圾回收启动之后,它就是空的,下面的2.4节给出具体的验证。

    2.4 验证WeakSet的弱引用

    方法一:

            let obj = {name: 'Leozi'}
            let ws = new WeakSet()
            ws.add(obj)
            obj = null
            console.log(ws.has(obj))   // false
    
    • 1
    • 2
    • 3
    • 4
    • 5

    方法二:

            global.gc();  // global.gc() 强制节点运行垃圾回收
            console.log(process.memoryUsage())  // process.memoryUsage() Nodejs的内存占用情况 此时heapUsed ≈ 2M
            
            let obj = { name: 'LLL', age: new Array(5 * 1024 * 1024) } // 为了差距明显一点,这里多加一个值让整个对象占据内存更大
            let ws = new WeakSet();
            ws.add(obj);
            
            global.gc();
            console.log(process.memoryUsage())  // 此时heapUsed ≈ 4.5M,说明obj占用了内存且没有被回收
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    上面的代码存在obj的外部引用,下面取消obj引用后看看最终的内存占用

            global.gc();  // global.gc() 强制节点运行垃圾回收
            console.log(process.memoryUsage())  // process.memoryUsage() Nodejs的内存占用情况 此时heapUsed ≈ 2M
            
            let obj = { name: 'LLL', age: new Array(5 * 1024 * 1024) } // 为了差距明显一点,这里多加一个值让整个对象占据内存更大
            let ws = new WeakSet();
            ws.add(obj);
            
            obj = null;  // 取消obj的外部引用
            global.gc();
            console.log(process.memoryUsage())  // 此时heapUsed ≈ 2M,说明obj没有占用了内存且已经被回收
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    创建完js文件之后,在命令行输入 node --expose-gc set.js 命令执行 set.js 中的代码,其中 --expose-gc 参数表示允许手动执行垃圾回收机制。

    2.5 WeakSet的应用场景

    储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏,下面用代码进行说明。

            let wrap = document.getElementById('wrap')
            let btn = document.getElementById('btn')
    
            // 假如想给这个btn加上"禁用"标签,就把它放在Set结构中
            const disableElements = new Set()
            disableElements.add(btn)
            btn.addEventListener('click', () => {
                wrap.removeChild(btn)
            })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    假设我们需要给记录页面上的禁用标签,那么一个Set对象存放就可以了,这样写功能上没有问题,但如果写成这样,当点击事件发生后,button 的dom被移除,那么整份js中 disabledElements 这个对象因为是强引用,其中的值依然存在于内存中的,那么内存泄漏就造成了,于是我们可以换成 WeakSet 来存放。

            let wrap = document.getElementById('wrap')
            let btn = document.getElementById('btn')
    
            // 假如想给这个btn加上"禁用"标签,就把它放在Set结构中
            const disableElements = new WeakSet()
            disableElements.add(btn)
            btn.addEventListener('click', () => {
                wrap.removeChild(btn)
            })
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    这里当 button 被移除,disabledElements中的内容会因为是弱引用而直接变成空,也就是disabledElements被垃圾回收掉了其中的内存,避免了内存泄漏的产生。

    三、映射Map

    ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合。但是“键”
    的范围不限于字符串,各种类型的值(包括对象)都可以当作键。Map 也实现了
    iterator 接口,所以可以使用『扩展运算符』和『for…of…』进行遍历。Map 的属
    性和方法:

    • size 返回 Map 的元素个数
    • set 增加一个新元素,返回当前 Map
    • get 返回键名对象的键值
    • delete 删除某个键值对
    • has 检测 Map 中是否包含某个元素,返回 boolean 值
    • clear 清空集合,返回 undefined
    3.1 基本API
            let m = new Map()
            m.set('name', 'Leozi')
            m.set('change', function() {
                console.log('changed')
            })
            let key = {
                Leozi: 'Leozi'
            }
            m.set(key, ['AA', 'BB', 'CC'])
    
            console.log(m.size)
    
            m.delete('name')
            console.log(m)
    
            console.log(m.get('change'))
            console.log(m.get(key))
    
            // m.clear()
    
            for(let v of m) {
                console.log(v)
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    在这里插入图片描述


    四、弱映射WeakMap

    4.1 基本API
    • set 增加一个新元素,返回当前 WeakMap
    • get 返回键名对象的键值
    • delete 删除某个键值对
    • has 检测 WeakMap 中是否包含某个元素,返回 boolean 值
    • WeakMap中的键值对任何时候都可能被销毁,所以没必要提供迭代其键值对的能力,故WeakMap不支持迭代,并且也没有clear和size接口。
            const wm1 = new WeakMap(),
                  wm2 = new WeakMap(),
                  wm3 = new WeakMap();
            const o1 = {},
                  o2 = function(){},
                  o3 = window;
            
            wm1.set(o1, 37);
            wm1.set(o2, "azerty");
            wm2.set(o1, o2); // value可以是任意值,包括一个对象或一个函数
            wm2.set(o3, undefined);
            wm2.set(wm1, wm2); // 键和值可以是任意对象,甚至另外一个WeakMap对象
            
            wm1.get(o2); // "azerty"
            wm2.get(o2); // undefined,wm2中没有o2这个键
            wm2.get(o3); // undefined,值就是undefined
            
            wm1.has(o2); // true
            wm2.has(o2); // false
            wm2.has(o3); // true (即使值是undefined)
            
            wm3.set(o1, 37);
            wm3.get(o1); // 37
            
            wm1.has(o1);   // true
            wm1.delete(o1);
            wm1.has(o1);   // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    4.2 Map与WeakMap的区别
    • Map 对象的键可以是任何类型,但 WeakMap 对象中的键只能是对象引用;
    • WeakMap 不能包含无引用的对象,否则会被自动清除出集合(垃圾回收机制);
    • WeakMap 对象是不可枚举的,无法获取集合的大小。WeakMap不像 Map,
      一是没有遍历操作(即没有keys()、values()和entries()方法),也没有size 属性,也不支持clear方法,所以WeakMap只有四个方法可用get()、set()、has()、delete()

    Map的缺点:
    在JavaScript里,Map API可以通过共用两个数组(一个存放键,一个存放值)来实现。给这种 Map设置值时会同时将键和值添加到这两个数组的末尾。从而使得键和值的索引在两个数组中相对应。当从该Map取值的时候,需要遍历所有的键,然后使用索引从存储值的数组中检索出相应的值。但这样的实现会有两个很大的缺点,首先赋值和搜索操作都是O(n)的时间复杂度(n是键值
    对的个数),因为这两个操作都需要遍历全部整个数组来进行匹配。另外一个缺点是可能会导致内存泄漏,因为数组会一直引用着每个键和值。这种引用使得垃圾回收算法不能回收处理他们,即使没有其他任何引用存在了。
    相比之下,原生的WeakMap持有的是每个键对象的 “弱引用”,这意味着在没有其他引用存在时
    垃圾回收能正确进行
    。原生WeakMap的结构是特殊且有效的,其用于映射的key只有在其没有被
    回收时才是有效的。

    为什么WeakSet和WeakMap不支持迭代?
    因为for…of迭代是通过引用(强引用)的方式进行的,而WeakSet和WeakMap维护的是弱引用,也就是这些引用随时都可能被垃圾回收机制回收,如果可以迭代的话,则列表将会受到垃圾回收机制的影响,从而得不到不确定的结果。

    4.3 对于弱引用的理解

    定义:在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。
    在JavaScript中,一般创建一个对象var obj = new Object(),都是建立一个强引用,只有当
    我们手动设置 obj = null 的时候,才有可能回收 obj 所引用的对象。
    而如果我们能创建一个弱引用的对象,假如为var obj = new WeakObject(),
    我们什么都不用做,只用静静的等待垃圾回收机制执行,obj 所引用的对象就会被回收。
    例子

            const key = new Array(5 * 1024 * 1024);
            const arr = [
              [key, 1]
            ];
    
    • 1
    • 2
    • 3
    • 4

    使用这种方式,我们其实建立了arr对key所引用的对象(我们假设这个真正的对象叫Obj)的强引
    用。所以当你设置key = null时,只是去掉了key对Obj的强引用,并没有去除arr对Obj的强引用,所以Obj还是不会被回收掉。
    Map类型也是类似:

            let map = new Map();
            let key = new Array(5 * 1024 * 1024);
            
            // 建立了 map 对 key 所引用对象的强引用
            map.set(key, 1);
            // key = null 不会导致 key 的原引用对象被回收
            key = null;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    通过Node进行证明:

            // 允许手动执行垃圾回收机制  node --expose-gc
            
            global.gc();
            // 返回 Nodejs 的内存占用情况,单位是 bytes
            process.memoryUsage(); // heapUsed: 4640360 ≈ 4.4M
            
            let map = new Map();
            let key = new Array(5 * 1024 * 1024);
            map.set(key, 1);
            global.gc();
            process.memoryUsage(); // heapUsed: 46751472 注意这里大约是 44.6M
            
            key = null;
            global.gc();
            process.memoryUsage(); // heapUsed: 46754648 ≈ 44.6M
            
            // 这句话其实是无用的,因为 key 已经是 null 了
            map.delete(key);
            global.gc();
            process.memoryUsage(); // heapUsed: 46755856 ≈ 44.6M
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    如果你想要让Obj被回收掉,你需要先delete(key)然后再key = null:

            let map = new Map();
            let key = new Array(5 * 1024 * 1024);
            map.set(key, 1);
            map.delete(key);
            key = null;
    
    • 1
    • 2
    • 3
    • 4
    • 5

    通过Node进行证明:

            // node --expose-gc
            
            global.gc();
            process.memoryUsage(); // heapUsed: 4638376 ≈ 4.4M
            
            let map = new Map();
            let key = new Array(5 * 1024 * 1024);
            map.set(key, 1);
            global.gc();
            process.memoryUsage(); // heapUsed: 46727816 ≈ 44.6M
            
            map.delete(key);
            global.gc();
            process.memoryUsage(); // heapUsed: 46748352 ≈ 44.6M
            
            key = null;
            global.gc();
            process.memoryUsage(); // heapUsed: 4808064 ≈ 4.6M
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    4.4 WeakMap中弱引用的证明
            const wm = new WeakMap();
            let key = new Array(5 * 1024 * 1024);
            wm.set(key, 1);
            key = null;
    
    • 1
    • 2
    • 3
    • 4

    当我们设置 wm.set(key, 1) 时,其实建立了wm对key所引用的对象的弱引用,但因为let key = new Array(5 * 1024 * 1024)建立了key对所引用对象的强引用,被引用的对象并不会被回收,
    但是当我们设置 key = null 的时候,就只有wm对所引用对象的弱引用,下次垃圾回收机制执
    行的时候,该引用对象就会被回收掉。
    通过Node进行证明:

            // node --expose-gc
            
            global.gc();
            process.memoryUsage(); // heapUsed: 4638992 ≈ 4.4M
            
            const wm = new WeakMap();
            let key = new Array(5 * 1024 * 1024);
            wm.set(key, 1);
            global.gc();
            process.memoryUsage(); // heapUsed: 46776176 ≈ 44.6M
            
            key = null;
            global.gc();
            process.memoryUsage(); // heapUsed: 4800792 ≈ 4.6M
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    总结:弱引用的特性就是WeakMaps保持了对键名所引用的对象的弱引用,即垃圾回收机制不将该引用考虑在内。只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。也正是因为这样的特性,WeakMap内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6规定WeakMap不可遍历。

    4.5 WeakMap的应用场景

    1)在DOM对象上保存相关数据

            let wm = new WeakMap(), element = document.querySelector(".element");
            wm.set(element, "data");
            
            let value = wm.get(elemet);
            console.log(value); // data
            
            element.parentNode.removeChild(element);
            element = null;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    传统使用jQuery的时候,我们会通过 . d a t a ( ) 方 法 在 D O M 对 象 上 储 存 相 关 信 息 ( 就 比 如 在 删 除 按 钮 元 素 上 储 存 帖 子 的 I D 信 息 ) , j Q u e r y 内 部 会 使 用 一 个 对 象 管 理 D O M 和 对 应 的 数 据 , 当 你 将 D O M 元 素 删 除 , D O M 对 象 置 为 空 的 时 候 , 相 关 联 的 数 据 并 不 会 被 删 除 , 你 必 须 手 动 执 行 .data()方法在DOM对象上储存相关信息(就比如在删除按钮元素上储存帖子的ID信息),jQuery内部会使用一个对象管理DOM和对应的数据,当你将DOM元素删除,DOM对象置为空的时候,相关联的数据并不会被删除,你必须手动执行 .data()DOM(ID)jQuery使DOMDOMDOM.removeData()方法才能删除掉相关联的数据,WeakMap就可以简化这样的操作。

    2)数据缓存
    使用WeakMap可以将先前计算的结果与对象相关联,而不必担心内存管理。以下功能 countOwnKeys()是一个示例:它将以前的结果缓存在WeakMap中cache。

            const cache = new WeakMap();
            
            function countOwnKeys(obj) {
              if (cache.has(obj)) {
                return [cache.get(obj), 'cached'];
              } else {
                const count = Object.keys(obj).length;
                cache.set(obj, count);
                return [count, 'computed'];
              }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    测试:

            let obj = { name: "kakuqo", age: 30 };
            console.log(countOwnKeys(obj));
            // [2, 'computed']
            console.log(countOwnKeys(obj));
            // [2, 'cached']
            obj = null; // 当对象不在使用时,设置为null
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3)私有属性
    示例1:在以下代码中,WeakMap _counter和_action用于存储以下实例的虚拟属性的值

            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);
                counter--;
                _counter.set(this, counter);
                if (counter === 0) {
                  _action.get(this)();
                }
              }
            }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    测试:

            let invoked = false;
            
            const countDown = new Countdown(3, () => invoked = true);
            countDown.dec();
            countDown.dec();
            countDown.dec();
            
            console.log(`invoked status: ${invoked}`)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    示例2:

            const privateData = new WeakMap();
            
            class Person {
                constructor(name, age) {
                    privateData.set(this, { name: name, age: age });
                }
            
                getName() {
                    return privateData.get(this).name;
                }
            
                getAge() {
                    return privateData.get(this).age;
                }
            }
            
            export default Person;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
  • 相关阅读:
    04 JAR 文件规范
    Acwing算法提高课——背包问题求具体方案
    java毕业设计商店管理系统源码+lw文档+mybatis+系统+mysql数据库+调试
    SystemVerilog学习-06-类的封装
    CSS使两个不同的div居中对齐的三种解决方案
    编程狂人|金融业分布式数据库选型及HTAP场景实践
    C 学生管理系统 打印/修改指定位置信息
    【vue3|第20期】vue3中Vue Router路由器工作模式
    QT学习笔记(六)——QT弹出对话框并在主窗口调用对话框的信息
    ModaHub魔搭社区开源AI Agent开发框架和评测
  • 原文地址:https://blog.csdn.net/qq_41481731/article/details/125538557