ES6之WeakMap

突发奇想,写一个ES6提供的原生数据结构——WeakMap。为什么要讲它呢?因为它看起来特别的废柴(汗)。

WeakMap

相比于java和C++,Javascript util方法(或是说原生数据结构)简直是少而不精,WeakMap更是其中的“佼佼者”。我们来看一下它仅有的一些属性和方法:

  • 属性

    Only One。就一个WeakMap.prototype.constructor用于创建实例,用法如下:

    let wm = new WeakMap([[k1, v1], [k2, v2]]) // vm = {k1:v1, k2:v2}
    

    WeakMap初始化参数是一个Iterable的对象, 可以是二元数组或者其他可迭代的键值对的对象。每个键值对会被加到新的 WeakMap里。

    注意:WeakMap对Key有限制,它必须是Object(Symbol也不行)

  • 方法

    WeakMap的方法也特别少,只有四个:deletegethasset。以前还有个clear,后来被弃用了。且不说java的Map一来就是几十个方法;就连ES6同时期提供的Map也有十个方法。

    可能乍一看,少几个API并不会有什么特别大的影响,毕竟有了最基本的增删改查功能。但是,当你想用下面这些操作时,你真的会很绝望。

    wm.size // no such property
    
    wm.keys(); // no such function
    
    wm.forEach(...) // unable to be iterated
    

Map v.s. WeakMap

OK,那WeakMap这么废柴,它存在的意义是什么呢?

看一个场景:

var map = new Map();
var weakmap = new WeakMap();

(function IIFE(){
    var k1 = {x: 1};
    var k2 = {y: 2};

    map.set(k1, 'k1');
    weakmap.set(k2, 'k2');
})()

map.forEach((val, key) => console.log(key, val))
// Weakmap. forEach(...) ERROR!

我们思考一个很深(wu)层(liao)的问题,在运行完IIFE函数后,我们是否还需要在map里保存k1的对象呢?

答案应该是“不保存”:k1和k2的作用域在IIFE内,之后我们将无法获取这两个引用,再驻留map里只会产生副作用。但是IIFE之后,当遍历map时——map.forEach(...),我们依旧能找到{x: 1},而且除了调用clear方法,我们甚至无法删除这个对象;垃圾回收机制更无法对{x: 1}起作用,久而久之便是内存溢出。

具体原因还是得从Map api中深究。Map api共用了两个数组(一个存放key,一个存放value)。给Map set值时会同时将key和value添加到这两个数组的末尾。从而使得key和value的索引在两个数组中相对应。当从Map取值时,需要遍历所有的key,然后使用索引从存储值的数组中检索出相应的value。这个实现的缺点很大,首先是赋值和搜索的时间复杂度为O(n);其次是可能导致内存溢出,因为数组会一直保存每个键值引用,即便是引用早已离开作用域,垃圾回收器也无法回收这些内存。那WeakMap呢?(虽然就它那几个api,引用不存在后,WeakMap确实也没啥可以操作了)。

看一下WeakMap的polyfill,管中窥豹。

var WeakMap = function() {
    this.name = '__wm__' + uuid()
};

WeakMap.prototype = {
    set: function(key, value) {
        Object.defineProperty(key, this.name, {
            value: [key, value],
        });
        return this;
    },
    get: function(key) {
        var entry = key[this.name];
        return entry && (entry[0] === key ? entry[1] : undefined);
    },
    ...
};

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

Usecase

说实在WeakMap使用场景也不多(汗),硬要找的话还是有以下几种:

Cache

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

let wm = new WeakMap();

// API layer
router.post('/applicant', (req, res) => {
    let applicant = req.body;
    let tenant = req.header('tenant');
    vm.set(applicant, tenant);
    dao.save(applicant)
})

// DAO layer
class DAO {
    save( applicant ){
        let tenant = wm.get(applicant);
        DB.save( Object.assign(applicant, {primary-key: tenant}) );// tenant as Primary Key in DB
    }
}

DOM listener

管理DOM listener时也可以用WeakMap

const dom = {};
addListener(dom, () => console.log('hello'));
addListener(dom, () => console.log('world'));

triggerListeners(dom);

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

const listeners = new WeakMap();

function addListener(obj, listener) {
    if (!listeners.has(obj)) {
        listeners.set(obj, new Set());
    }
    listeners.get(obj).add(listener);
}

function triggerListeners(obj) {
    const listeners = listeners.get(obj);
    if (listeners) {
        for (const listener of listeners) {
            listener();
        }
    }
}

Private Data

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

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

小结

今天科普了一个ES6的新feature——WeakMap。表面看起来挺废柴的feature,现实开发中也很少能用到(汗);不过在内存敏感的场景下还是有一定用武之地的。虽然JS这类高级语言隐藏了很多内存管理的功能,但无论如何还是不能解决一些极端情况。这时候仍需开发人员自己注意一些内存细节。ECMAScript提出WeakMap(还有一个WeakSet)的概念,终于给了开发人员一种主动解决内存回收的方式。

相关播客

《Javascript垃圾回收机制》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349