JS中的WeakMap
一、WeakMap起源
ES6新增的“弱映射(WeakMap)是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap是Map的“兄弟类型””,其API也是Map的子集。WeakMap中的“weak”(弱),描述的是JavaScript垃圾回收程序对待“弱映射”中键的方式。
二、基本API
可以使用new关键字实例化一个空的WeakMap;
const wm=new WeakMap();
弱映射中的键只能是Object或者继承自Object的类型,尝试使用非对象设置键会抛出TypeError。值的类型没有限制。
如果想在初始化时候填充弱映射,则构造函数可以接收一个可迭代对象,其中需要包含键/值对数组。可迭代对象中的每个键/值都会按照迭代顺序插入新实例中:
const key1={id:1}, key2={id:2}, key3={id:3}; //使用嵌套数组初始化弱映射 const wm1=new WeakMap([[key1,"val1"],[key2,"val2"],[key3,"val3"]]); alert(wm.get(key1)); //val1 alert(wm.get(key2)); //val2 alert(wm.get(key3)); //val3 //初始化是全有或全无的操作 //只要有一个键无效就会抛出异常,导致整个初始化失败 const wm2=new WeakMap([[key1,"val"],["BADKEY","val2"],[key3,"val3"]]); //TypeError:Invalid value used as WeakMap key typeof wm2; //ReferenceError:wm2 is not defined //原始值可以先包装成对象再用作键 const stringKey=new String("key1"); const wm3=new String([stringKey,"vall"]); alert(wm3.get(stringKey)); //'vall'
初始化之后可以使用set()再添加键/值对,可以使用get()和has()查询,还可以使用delete()删除:
const wm=new WeakMap(); const key1={id:1}, key2={id:2}; alert(wm.has(key1)); //false alert(wm.get(key1)); //undefined wm.set(key1,"Matt").set(key2,"Frisble"); alert(wm.has(key1)); //true alert(wm.get(key1)); //Matt wm.delete(key1); //只删除这一个键/值对 alert(wm.has(key1)); //false alert(wm.has(key2)); //true
set()方法弱映射实例,因此可以把多个操作连缀起来,包括初始化声明:
const key1={id:1},key2={id:2},key3={id:3};
const vm=new WeakMap().set(key1,"val1");
wm.set(key2,"val2").set(key3,"val3");
alert(wm.get(key1)); //val1
alert(wm.get(key2)); //val2
alert(wm.get(ket3)); //val3
三、弱键
WeakMap中"weak"表示弱映射射的键“弱弱地拿着”的。意思就是,这些键不属于正式的引用,不会阻止垃圾回收。但要注意的是,弱映射中值的引用可不是“弱弱地拿着”的。只要键存在,键/值对就会存在于映射中,并被当作对值的引用,因此就不会被当作垃圾回收。
来看下面的例子:
const wm=new WeakMap(); wm.set({},"val");
set()方法初始化了一个新对象并将它用作一个字符串的键。因为i没有指向这个对象的其他引用,所以当这行代码执行完之后,这个对象键就会被当作垃圾回收。然后,这个键/值就从弱映射中消失了,使其成为了一个空映射。在这个例子中,因为值也没有被引用,所以这对键/值被破坏后,值本身也会成为垃圾回收的目标。
const wm=new WeakMap(); const container={ key:{} }; wm.set(container.key,"val"); function removeReference(){ container.key=null; }
这一次,container对象维护着一个对弱映射键的引用,因此这个对象键不会成为垃圾回收的目标。不过如果调用了removeReference(),就会摧毁键对象的最后一个引用,垃圾回收程序就可以把这个键/值对清理掉。
四、不可迭代键
因为WeakMap中键/值对任何时候都可能被销毁,所以没必要提供迭代其键/值对的能力。当然也用不着像clear()这样一次性销毁所有键/值的方法。WeakMap确实没这个方法。因为不可能迭代,所以也不可能在不知道对象引用的情况下从弱映射中取得值。即便代码可以访问WeakMap实例,也没办法看到其中的内容。
WeakMap实例之所以限制只能用对象作为键,是为了保证只有通过键对象的引用才能取得值。如果允许原始值,那就没办法区分初始化使用的字符串字面量和初始化之后使用的一个相等的字符串了。
五、Map和WeakMap区别
1、WeakMap只接受对象作为key,如果设置其他类型的数据作为key,会报错。
2、WeakMap的key所引用的对象都是弱引用,只要对象的其他引用被删除,垃圾回收机制就会释放该对象占用的内存,从而避免内存泄漏。
3、由于WeakMap的成员随时可能被垃圾回收机制回收,成员的数量不稳定,所以没有size属性。
4、没有clear()方法
5、不能遍历
6、WeakMap限制只能用对象作为键的原因⭐⭐⭐
《JavaScript高级程序设计第四版》说的:是为了保证只有通过键对象的引用才能取得值,如果允许原始值,那就没办法区分初始化时使用的字符串字面量和初始化之后使用的一个相等的字符串。书上P170
理解不来
下面是其他人的理解
因为基础类型是按值传递的。给一个 Map 实例设置一个基础类型的变量为键,就意味着是把这个变量的值复制了一份给 Map 实例,实例中的键与原来的这个变量就失去联系了,即便原来的这个变量的内存被回收了,实例中的键也还存在,重新创建一个同值的变量就可以获得键值对的信息。
而引用类型是按引用传递的。实例中的键其实是对变量内存地址的弱引用(此“弱引用”不影响内存回收,与通常意义上的引用不同),变量一旦被回收,WeakMap 中的“引用”也就被销毁了,大致是这个意思。
对于 Map 而言,内存回收的问题需要开发者自己解决,所以用基础类型没有问题,而开发使用 WeakMap 的动机之一,就是键名失去引用之后能自动回收内存(比如可以用来保存DOM节点,不容易造成内存泄漏,书上P172)
7、使用弱映射
WeakMap实例与现有JavaScript对象有着很大的不同,可能一时不容易说清楚应该怎么使用它。这个问题没有唯一的答案,但有已经出现了很多相关策略。
1).私有变量
弱映射造就了在JavaScript中实现真正私有变量的一种新方式。前提很明确:私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
const wm=new WeakMap(); class User{ constructor(id){ this.idProperty=Symbol("id"); this.setId(id); } setPrivate(property,value){ const privateMembers=wm.get(this) || {}; privateMembers[property]=value; vm.set(this,privateMembers); } getPrivate(property){ return wm.get(this)[property]; } setId(id){ this.setPrivate(this.idProperty,id); } getId(){ return this.getPrivate(this.idProperty); } const user=new User(123); alert(user.getId()); //123 user.setId(456) alert(user.getId()); //456
//并不是真正私有的
alert(wm.get(user)[user.idProperty])
对于上面的实现,外部代码只需要拿到对象实例的引用和弱映射,就可以取得"私有'变量了。为了避免这种访问,可以用一个闭包把WeakMap包装起来,这样就可以把弱映射与外界完全隔离开了:
class User{
const wm=new WeakMap(); constructor(id){ this.idProperty=Symbol("id"); this.setId(id); } setPrivate(property,value){ const privateMembers=wm.get(this) || {}; privateMembers[property]=value; vm.set(this,privateMembers); } getPrivate(property){ return wm.get(this)[property]; } setId(id){ this.setPrivate(this.idProperty,id); } getId(){ return this.getPrivate(this.idProperty); }
}
return User;
})();
const user=new User(123); alert(user.getId()); //123 user.setId(456) alert(user.getId()); //456
这样,拿不到弱映射中的键,也就无法取得弱映射中对应的值。虽然这防止了前面提到的访问,但整个代码也完全陷入了ES6之前的闭包私有变量模式。
2)DOM节点元数据
因为WeakMap实例不会妨碍垃圾回收,所以非常适合保存关联元数据。来看下面这个例子,其中使用了常规的Map;
const m=new Map(); const loginButton=document.querySelector('#login'); //给这个节点关联一些元数据 m.set(loginButton,{disabled:true});
假设上面的代码执行后,页面被JavaScript改变了,原来的登录按钮从DOM树中被删掉了。但由于映射中还保存着按钮的引用,所以对应的DOM节点仍然会逗留在内存,除非明确将其从映射中删除或者等到映射本身被销毁。
如果这里使用的是弱映射,如以下代码所示,那么当节点从DOM数中被删除后,垃圾回收程序就可以立即释放其内存
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?