MZhou's blog

ECMAScript 6之WeakMap

ECMAScript 6中加入了很多新的特性,其中有一个有用的API:WeakMap。Nicholas的博文做了详细的介绍。这也是一篇关于WeakMap的笔记。


简介

WeakMapMap(另一个ES6的新API)都是键值对象,有着类似的API:setgethasdelete。为了更好的解释WeakMap,先谈下Javascript中的键值对象。JS中几乎所有可写的对象就可以当作一个键值对象,例如:

var map = {};
map['key'] = 'value';

但是这样简单的键值对象达不到一个效果:它的键“不能”是对象。之所以打引号是因为并不是不能用对象,而是对象会转换成字符串,如下:

var map = {};
var key = {};
map[key] = 'value';
map['[object Object]'] === 'value'; // true

于是ES6就提供了MapWeakMap的API,可以实现键是对象的情况。但是这引来了另外一个问题,Map是可以通过keys方法来获取其所有的键。这就需要map保留一个对键的强引用,于是就导致了引用释放上的不方便,稍微大意一些就容易造成内存泄漏。

于是又提供了WeakMap,它的键只能是一个非空(null)对象,而Map的键则还可以是除对象外的原始类型。WeakMap的重要特点在于:对键的引用是弱引用,而不是一般的强引用。看如下例子:

var map = new WeakMap(),
element = document.querySelector(".element");

// 对象element作为键,"Original"作为值
map.set(element, "Original");

// 可以通过element获得值
var value = map.get(element);
console.log(value);             // "Original"

// 下面删除对element的强引用
element.parentNode.removeChild(element);
element = null;

// element dom对象被释放
// 已经无法通过element获取“Original”值

WeakMap的键只能是非空(null)对象,所以如果你在get方法里面传入null或是undefined之类的值,就会报错:“value is not a non-null object”。它的另外一个特点就是无法获取所有的键,更无法获取size之类的值。

WeakMap一个典型的应用场景就是对于jQuery这样的库,需要维护一个dom列表,存储对应每个dom的数据。这时候使用它就可以达到“dom在文档中被移除的时候,自动释放dom对象”。

当然ES6目前还没有普及,支持的浏览器仅有Firefox和Chrome。在chrome中你需要到chrome://flags,并且启用“Experimental JavaScript Features”。所以我们需要fallback。

Fallback

Gozala提供了一个很精妙的fallback方案!接下来对它的代码做解读。要实现WeakMap有几个关注点:

  1. 键一定是非空对象
  2. 键无法被获取到
  3. WeakMap不能保留对键的强引用

因为这个方案是基于ECMAScript 5的,所以使用了Object.freezeObject.create之类的API。WeakMap的初始化函数如下:

function WeakMap() {
  /**
  Implementation of harmony `WeakMaps`, in ES5\. This implementation will
  work only with keys that have configurable `valueOf` property (which is
  a default for all non-frozen objects).
  http://wiki.ecmascript.org/doku.php?id=harmony:weak_maps
  **/

  var privates = Name()
  return Object.freeze(Object.create(WeakMap.prototype, {
    has: {
      value: function has(object) {
        return 'value' in privates(object)
      },
      configurable: true,
      enumerable: false,
      writable: true
    },
    get: {
      value: function get(key, fallback) {
        return privates(guard(key)).value || fallback
      },
      configurable: true,
      enumerable: false,
      writable: true
    },
    set: {
      value: function set(key, value) {
        privates(guard(key)).value = value
      },
      configurable: true,
      enumerable: false,
      writable: true
    },
    'delete': {
      value: function set(key) {
        return delete privates(guard(key)).value
      },
      configurable: true,
      enumerable: false,
      writable: true
    }
  }))
}

使用了Object.create创建了一个继承自WeakMap的新对象,并设置了has,get,set,delete方法。(其实我不太理解delete是个关键字,为什么标准要把它作为WeakMap的方法名之一?)然后使用Object.freeze确保对象无法被改变。避免有人通过map.a = 'string'这样或其他方式去修改对象。

其中的关键点在于var privates = Name()一句。privates方法是用来“依据键获取键值”。

因为不能保存键的强引用所以需要将键值保存在键上。即WeakMap仅仅是提供了一个“键”与“值”之间的接口,通过WeakMap来设置或获取键对应值。这么一来就容易理解了,具体源代码如下:

function defineNamespace(object, namespace) {
  /**
  Utility function takes `object` and `namespace` and overrides `valueOf`
  method of `object`, so that when called with a `namespace` argument,
  `private` object associated with this `namespace` is returned. If argument
  is different, `valueOf` falls back to original `valueOf` property.
  **/

  // Private inherits from `object`, so that `this.foo` will refer to the
  // `object.foo`. Also, original `valueOf` is saved in order to be able to
  // delegate to it when necessary.
  var privates = Object.create(object), base = object.valueOf
  Object.defineProperty(object, 'valueOf', { value: function valueOf(value) {
    // If `this` or `namespace` is not associated with a `privates` being
    // stored we fallback to original `valueOf`, otherwise we return privates.
    return value != namespace || this != object ? base.apply(this, arguments)
                                              : privates
  }, configurable: true })
  return privates
}

function Name() {
  /**
  Desugared implementation of private names proposal. API is different as
  it's not possible to implement API proposed for harmony with in ES5\. In
  terms of provided functionality it supposed to be same.
  http://wiki.ecmascript.org/doku.php?id=strawman:private_names
  **/

  var namespace = {}
  return function name(object) {
    var privates = object.valueOf(namespace)
    return privates !== object ? privates : defineNamespace(object, namespace)
  }
}

function guard(key) {
  /**
  Utility function to guard WeakMap methods from keys that are not
  a non-null objects.
  **/

  if (key !== Object(key)) throw TypeError("value is not a non-null object")
  return key
}

代码中guard方法用于验证键是否是非空对象,又通过重写valueOf方法来获取键对应的存储值的对象。即是如下的引用关系:

key -> valueOf --(namespace)--> privates --> value

其中的关键点是namespace是一个空对象。为什么呢?因为一个空对象是无法模仿的、是唯一的,如果改用字符串则很容易被知道字符串值的人直接通过key.valueOf('namespace')方法获取到privates。

所以实现WeakMap的核心是“如何构造一个唯一的无法伪造的属性名”。

Nicolas还在Gozala方案的评论中提到了另外一种实现,具体见http://code.google.com/p/es-lab/source/browse/trunk/src/ses/WeakMap.js。我还没有仔细看~

MZhou's blog - Taste of life.

zmmbreeze / @zhoumm