【ES6的标准入门】JavaScript中Set、Map与弱引用版本:WeakSet和WeakMap优雅的数据管理技巧

在这里插入图片描述

😁 作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:JavaScript进阶指南
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气

​🔥​前言:

这里是关于map和set这两种数据类型的学习,因为这两种数据类型延申出来了Weak类的数据类型,大家可以多关心这种数据类型的使用, 这是我自己的学习JavaScript的笔记,希望可以帮助到大家,欢迎大家的补充和纠正

第11章 Set和Map数据结构

11.1 Set

11.1.1 基本用法

ES6提供了新的数据结构——Set 它类似于数组,但是成员的值都是唯一的,没有重复

const s=new Set();

[2,3,5,4,5,2,2].forEach(x=> s.add(x))

for(let i of s){
    console.log(i)
}

//2 3 5 4

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

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

const set=new Set([1,2,3,4,4,])
[...set]
//[1,2,3,4]

使用细节:

  1. 向Set加入值时不会发生类型转换,所以5和’5“ 是两个不同的值,Set内部判断两个值是否相同时使用的算法叫做 ”Same-value equality" 它类似于精确相等运算符(===),主要的区别是 NaN 等于自身,而精确相等运算符认为NaN不等于自身
  2. 两个对象总是不相等的
11.1.2 Set实例的属性

Set结构的实例有以下属性

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

Set实例的方法分为两大类:操作方法和遍历方法

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

⭐️ 使用细节:

  1. 可以链式调用

    s.add(1).add(2).add(2)
    
  2. Array.from方法可以将Set结构转为数组

11.1.4 Set实例的遍历操作

Set结构的实例有四个遍历方法,可以遍历成员

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

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

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

遍历方法的应用:

  1. 扩展运算符和Set结构相结合就可以去除数组的重复成员

    let arr=[3,5,2,2,5,5]
    let unique=[...new Set(arr)]
    //[3,5,2]
    
  2. ⭐️ 因此使用Set可以容易地实现并集,交集和差集

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

11.2 WeakSet

11.2.1 含义

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

第一,WeakSet的成员只能是对象,而不是是其他类型的值。

第二,WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑对象是否还存在于WeakSet之中

⭐️ 什么是垃圾回收机制:

垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这快内存,结果使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄露,WeakSet里面的引用都不计入垃圾回收机制,所以不存在这个问题,因此WeakSet适合临时存放一组对象,以及存放跟对象绑定的信息,只要这些对象在外部消失,它在WeakSet里面的引用就会自动消失

11.2.2 语法

WeakSet是一个构造函数,可以使用new命令创建WeakSet数据结构,并且可以接受一个数组或类似数组的对象作为参数,该数组所有成员都会自动

const ws =new WeakSet()

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

上面的 代码中,a是一个数组,它有两个成员,也都是数组,将a作为WeakSet构造函数的参数,a成员会自动成为WeakSet的成员

❗️ 注意:

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

const b=[3,4]
const ws=new WeakSet(b)
//Uncaught TypeError:Invalid value user in weak set(...)

上面的代码中,数组b的成员不是对象,因此加入WeakSet就会报错

WeakSet结构有以下3个方法

  • WeakSet.prototype.add(value):向WeakSet实例添加一个新成员
  • WeakSet.prototype.delete(value):清除WeakSet实例的指定成员
  • WeakSet.prototype.has(value):返回一个布尔值,表示某一个值是否在WeakSet实例中。
11.2.3 应用
  • WeakSet的一个用处是储存DOM节点,而不用担心这些节点从文档移出时会引发内存泄露

11.3 Map

11.3.1 含义和基本用法

ES6提供了Map数据结构,它类似于对象,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键

📑 提供Map数据结构的背景原因:

JavaScript的对象本质上时键值对的集合(Hash结构)但是只能用作字符串作为键,这在使用上带来了极大的限制,例如:想要使用Dom对象作为键的名就不可以,可自动转为字符串

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(o) //fasle

Map作为构造函数,也可以接受一个数组(任何具有Iterator接口且每个成员都是一个双元素数组)作为参数,该数组的成员是一个个表示键值对的数组

const map=new Map([
	['name','张三'],
    ['title','Author']
])

const set=new Set([
    ['foo',1],
    ['bar',2]
])
const m1=new Map(set)

map.get('name') //张三
m1.get('foo') //1

使用细节:

  1. 如果对同一个键多次赋值,后面的值将覆盖前面的值
  2. 如果读取一个未知的键,则返回undefined

❗️ 注意:

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

const map=new Map()

map.set(['a'],555)
map.get(['a']) //undefined

上面的代码中的set和get方法表面上针对的是同一个键,实际上却是两个值,内存地址 不一样。

下面这个实例或许更加清晰:

const map=new Map()

const k1=['a']
const k2=['a']

map.set(k1,111).set(k2,222)

map.get(k1) //111
map.get(k2) //222

上面的代码中,变量k1和k2的值是一样的,但它们在Map结构中被视为两个键

由上可知,Map的键实际上是和内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞的问题,我们扩展别人库时,如果使用对象作为键名,不用担心自己的属性与原作者的属性同名。

如果Map的键是一个简单类型的值(数字,字符串,布尔值),则只要两个值严格相等,Map就视为一个键,包括0和-0。另外,虽然NaN不严格等于自身,但Map将其视为同一个键

11.3.2 实例的属性和方法
  • size属性:返回Map结构成员总数
  • set(key,value):设置key所对应的键值,然后返回整个Map结构,如果key已经有值,则键值会被更新,否则就新生成该键
  • get(key):读取key对应的键值,如果找不到key,返回undefined。
  • has(key):返回一个布尔值,表示某个键是否在Map数据结构中
  • delete(key):删除某个键,返回true,如果删除失败,则返回false
  • clear():清除所有成员,没有返回值
11.3.3 遍历方法

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

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

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

11.3.4 与其他数据结构互相转换

1. Map转为数组

Map转为数组最方便的方法就是使用扩展运算符(…)

const myMap=new Map().set(true,7).set({foo:3},['abc'])

[...myMap]

2. 数组转为Map

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

new Map([
[true,7],
[{foo:3},['abc']]
])

3. Map转为对象

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

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)

4. 对象转为Map

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})

5. Map转为JSON

这种需求要区分两种情况

  • 转为JSON对象

    Map的键名都是字符串,这时可以选择转为对象JSON

    思路:map => 对象 => JSON

    function strMapToJson(strMap){
        return JSON.stringify(strMapToObj(strMap))
    }
    
    let myMap=new Map().set('yes',true).set('no',false)
    strMapToJson(myMap)
    
  • 转为数组JSON

    Map的键名有非字符串,这时可以选择转为数组JSON

    function mapToArrayJson(map){
        return JSON.stringify([...map])
    }
    
    let myMap=new Map().set(true,7).set({foo:3},['abc'])
    mapToArrayJson(myMap)
    

6. JSON转为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))
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')

11.4 WeakMap

11.4.1 含义

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

1. WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名

2. WeakMap的键名所指向的对象不计入垃圾回收机制

有关第二点,可以仔细阐述一下:

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

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

在arr数组中,有两个对象,加入我们不再需要这两个对象,我们必须手动删除这个引用,否则垃圾回收机制就不会释放el和e2占用的内存

//不需要el和e2的时候,必须手动删除引用
arr[0]=null
arr[1]=null

上面这种写法显然很不方便,一旦忘了写,就会造成内存泄露

WaekMap就是为了解决这个问题诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除了,垃圾回收机制就会释放该对象所占用的内存。

总的来说:如果要向对象中添加数据又不想干扰垃圾回收机制,便可以使用WeakMap

❗️ 注意:WeakMap弱引用只是键名而不是键值,键值依然是正常引用

例如以下代码:

const wm=new WeakMap()
let key={}
let obj={foo:1}

wm.set(key,obj)
obj=null
wm.get(key)
//Object {foo:1}
11.4.2 WeakMap的语法

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

  • 没有遍历操作
  • 无法清空,即不支持clear方法
11.4.3 WeakMap示例

WearMap的例子很难演示,因为无法观察它里面的引用自动消失,此时,其他的引用都已解除,已经没有引用指向WeakMap的键名,导致无法证实键名是不是存在

贺师俊老师提示,如果引用所指向的值值占用特别多的内存,就可以通过Node的peocess.memoryUsage方法看出来

根据以上思路,可以有以下的例子:

首先,打开Node命令行

$ node --expose-gc

上面的代码中,表示允许手动执行垃圾回收机制

然后执行以下代码

// 手动执行一次垃圾回收,保证获取的内存使用状态是正确的
> global.gc();
undefined

//查看内存占用的初始状态,heapUsed为4MB左右
> process.memoryUsage()
{
	rss:21106688,
	heapTotal:7376896,
	heapUsed:4153936,
	external:9095
}

> 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增加到45MB了
> process.memoryUsage()
{
	rss:67538944,
	heapTotal:7376896,
	heapUsed:45782816,
	external:8945
}

//清除变量key对数组的引用
//但没有手动清除WeakMap实例的键名对数组的引用
> key=null
null

//再次执行垃圾回收机制
> global.gc()
undefined

//内存占用heapUser变回4MB左右
//可以看到WeakMap的键名引用没有阻止gc对内存的回收
> process.memoryUsage()
{
	rss:20639744,
	heapTotal:8425472,
	heapUsed:3979792,
	external:8956
}

上面的代码中,只要外部的引用消失,WeakMap内部的引用就会自动被垃圾回收清除,由此可见,有了WeakMap的帮助,解决内存泄露就会简单很多

11.4.4 WeakMap用途

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

let myElement=document.getElementById('logo')
let myWeakmap=new WeakMap()

myWeakmap.set(myelement,{times Clicked:0});

myElement.addEventListener('click',function(){
    let logoData=myWeakmap.get(myElement)
    logoData.timesClicked++
},false)

上面的代码中,myElement是一个DOM节点,每当click事件发生就更新一下状态,我们将这个状态作为键值放到WeakMap里,对应的键名就是myElement,一旦这个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. 部署私有属性

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是实例的弱引用,如果删除实例,它们也会随之消失,不会造成内存泄露

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值