Map v.s. WeakMap

  1. var map = new Map();
  2. var weakmap = new WeakMap();
  3. (function IIFE(){
  4. var k1 = {x: 1};
  5. var k2 = {y: 2};
  6. map.set(k1, 'k1');
  7. weakmap.set(k2, 'k2');
  8. })()
  9. map.forEach((val, key) => console.log(key, val))
  10. // Weakmap. forEach(...) ERROR!

我们思考一个问题,在运行完 IIFE 之后,内存中是否还需要在map 中保存k1 这个对象呢?
答案是 不需要
k1 和 k2 的作用域在IIFE 内,之后我们再无法获取这两个引用,再驻留map 只会产生副作用。但是在IIFE 之后,当遍历map,map.forEach(…),我们依旧能找到{x: 1},而且除了调用clear方法,我们甚至无法删除这个对象;垃圾回收机制更无法对{x: 1}起作用,久而久之便是内存溢出。

  1. let map = new Map()
  2. let k1 ={}
  3. map.set(k1,'123')
  4. k1 = null
  5. console.log('map',map)
  6. let weakMap = new WeakMap()
  7. let k2 ={}
  8. weakMap.set(k2,'1234')
  9. k2=null
  10. console.log('weakMap',weakMap)

数s后(gc执行)
image.png
weakMap没了
也就是什么时候需要weakMap呢?
我需要一个容器,当他外部引用没了,他所有的啥啥都没了
———-DOM 当dom节点删了,dom的事件啥的都销毁了

weakMap的实现原理

  1. var WeakMap = function() {
  2. this.name = '__wm__' + uuid()
  3. };
  4. WeakMap.prototype = {
  5. set: function(key, value) {
  6. Object.defineProperty(key, this.name, {
  7. value: [key, value],
  8. });
  9. return this;
  10. },
  11. get: function(key) {
  12. var entry = key[this.name];
  13. return entry && (entry[0] === key ? entry[1] : undefined);
  14. },
  15. ...
  16. };

也就是说WeakMap 并没有像Map一样使用任何数组,weakmap.set(key, val)事实上是直接通过Object.defineProperty给这个key加了一个新属性——this.name,这就解释了为什么WeakMap的key必需是个Object了;同理,weakmap.get(key)是从key的该属性里获取了值对象。很有趣的设计。相比Map,WeakMap持有的只是每个键值对的“弱引用”,不会额外开内存保存键值引用。这意味着在没有其他引用存在时,垃圾回收器能正确处理key指向的内存块。正因为这个特殊的实现,WeakMap的key是不可枚举的,更不用说提供keys()、forEach()这类方法了。

使用场景

Cache

作缓存的话,一般是做全局Map,可以读取调用链上游的一些信息,好处就是调用链结束后随时可以回收内存。

  1. let wm = new WeakMap();
  2. // API layer
  3. router.post('/applicant', (req, res) => {
  4. let applicant = req.body;
  5. let tenant = req.header('tenant');
  6. vm.set(applicant, tenant);
  7. dao.save(applicant)
  8. })
  9. // DAO layer
  10. class DAO {
  11. save( applicant ){
  12. let tenant = wm.get(applicant);
  13. DB.save( Object.assign(applicant, {primary-key: tenant}) );// tenant as Primary Key in DB
  14. }
  15. }

DOM listener

管理DOM listener时也可以用WeakMap

  1. const dom = {};
  2. addListener(dom, () => console.log('hello'));
  3. addListener(dom, () => console.log('world'));
  4. triggerListeners(dom);

添加和触发监听器是很典型的订阅发布模式。实现时我们可以利用WeakMap保存listener,在DOM销毁后即可释放内存:

  1. const listeners = new WeakMap();
  2. function addListener(obj, listener) {
  3. if (!listeners.has(obj)) {
  4. listeners.set(obj, new Set());
  5. }
  6. listeners.get(obj).add(listener);
  7. }
  8. function triggerListeners(obj) {
  9. const listeners = listeners.get(obj);
  10. if (listeners) {
  11. for (const listener of listeners) {
  12. listener();
  13. }
  14. }
  15. }

Private Data

Javascript class暂时还没设计私有方法和私有变量,WeakMap是可以作为实现OO封装的方式之一。

  1. const _counter = new WeakMap();
  2. const _action = new WeakMap();
  3. class Countdown {
  4. constructor(counter, action) {
  5. _counter.set(this, counter);
  6. _action.set(this, action);
  7. }
  8. dec() {
  9. let counter = _counter.get(this);
  10. if (counter < 1) return;
  11. counter--;
  12. _counter.set(this, counter);
  13. if (counter === 0) {
  14. _action.get(this)();
  15. }
  16. }
  17. }

测试

  1. let cd = new Countdown(1,()=>{console.log('action')})
  2. cd.dec()// action