翻译自 Private instance members with weakmaps in JavaScript by Nicholas C. Zakas

在JavaScript中使用weakmaps实现私有实例成员

  1. 传统的私有成员实现方式

  2. 实现真正的私有成员

  3. 引入weakmap

  4. 结论

  5. 参考文献

上周,我阅读了Nick Fitzgerald的一篇描述如何使用ECMAScript 6 weakmaps 来实现在JavaScript中创建私有实例成员的文章。说实话,我从来不是weakmaps的坚定支持者 - 我想这没什么大惊小怪的,而且它们只有一个用途(跟踪DOM元素的数据)。我一直坚持这种想法,直到我读到了尼克的文章,在这一刻,我对weakmaps的想法改变了。现在,我看到了weakmaps给JavaScript带来的可能性以及它将如何在我们可能还无法完全想象之处改变我们的编码实践。这也是在尼克的文章所述之外的,这篇文章的重点之一。

传统的私有成员实现方式

JavaScript的一个最大缺点就是无法使用自定义类型来创建真正的私有实例成员。唯一的好办法是在构造函数内部创建一个私有变量并创建访问这些私有变量的特权方法,如:

function Person(name) {
    this.getName = function() {
        return name;
    };
}

在这个例子中, getName()方法使用name参数(它实际上是一个局部变量)来返回人名而并没有像一个属性一样公开name。这种做法确实是不错的,但是在你有大量的Person实例的情况下效率会非常低,因为每个实例都必须保存一份自己的getName()方法而不是从原型中继承它。

当然,你可以选择一个替代方法,依靠惯例来声明私有成员,很多人用一个下划线前缀来代表私有成员名称。下划线是不是魔术,它不会阻止任何人使用该成员,而是作为一个提醒的东西让你知道它不应该被使用。例如:

function Person(name) {
    this._name = name;
}

Person.prototype.getName = function() {
    return this._name;
};

这样的模式效率会更高,因为每个实例将使用相同的原型中的方法。该方法会访问this._name ,当然,这个对象也可以通过外部访问,但我们都约定好不这样做。这不是一个理想的解决方案,但它是一个很多开发者所选择的,依赖于一些保护措施的方案。

还有跨实例共享成员的情况,这很容易使用包含构造函数的立即调用的函数表达式(IIFE)来实现,例如:

var Person = (function() {

    var sharedName;

    function Person(name) {
        sharedName = name;
    }

    Person.prototype.getName = function() {
        return sharedName;
    };

    return Person;
}());

在这里, sharedName将在Person 的所有实例中共享,并且,每一个新实例将使用传入的name覆盖它的值。这显然​​是一个没什么意义的例子,但是它是理解如何实现真正的私有成员的重要的第一步。

实现真正的私有成员

共享的私有成员的模式指出了一个潜在的解决方案:如果私有数据没有存储在实例中,但该实例可以访问它,那不就行了吗?如果存在一种能对所有的实例隐藏私有信息的对象呢。在ECMAScript 6出现之前,你可能会这么做:

var Person = (function() {

    var privateData = {},
        privateId = 0;

    function Person(name) {
        Object.defineProperty(this, "_id", { value: privateId++ });

        privateData[this._id] = {
            name: name
        };
    }

    Person.prototype.getName = function() {
        return privateData[this._id].name;
    };

    return Person;
}());

现在,我们取得了一些进展。该privateData对象无法从IIFE的外部访问,它完全隐藏了包含在其中的所有数据。该privateId变量存储下一个实例可用的ID。不幸的是,该ID需要存储在实例上,所以最好以确保它不能被以任何方式改变,因此我们利用Object.defineProperty()来设置它的初始值,并确保该属性是不可写,不可配置,且不可枚举的。这样就保护了_id不被篡改。然后,在getName()内,方法使用_id来从私有数据集合中获取需要的数据并将其返回。

这种方法是一个相当不错的对于实例私有数据问题的解决方案,除了那个丑陋的附加在该实例上的_id。然而,这个方案也被一个问题所困扰,即所有数据都会持久存在即使该实例已经被垃圾回收的问题。然而,这种模式是我们根据ECMAScript 5标准所能做的最好的一个了。

引入weakmap

通过引入weakmap,在前面的例子中的“差不多完美了,还差一点”的状态自然被补全了。Weakmaps解决了私有数据成员的遗留问题。首先,再也没有必自己生成一个唯一的ID了,因为该对象实例本身就是一个唯一ID。其次,当一个对象实例被垃圾回收,绑到该实例中的weakmap中所有数据也会被回收。基本模式与前面的示例差不多,但现在的干净多了:

var Person = (function() {

    var privateData = new WeakMap();

    function Person(name) {
        privateData.set(this, { name: name });
    }

    Person.prototype.getName = function() {
        return privateData.get(this).name;
    };

    return Person;
}());

privateData在这个例子中是一个WeakMap的实例 。当一个新的Person被创建时,一个weakmap的条目会被创建用来以便该实例来保存包含私有数据的对象。在weakmap中最关键的是this ,即使对于开发者来说获取一个Person对象的引用是微不足道的一件事,他们也无法从实例外来访问到privateData,所以,数据被从麻烦制造者手中安全保护了。任何想要操纵私有数据的方法只能够通过传入实例的this ,从而拿到返回的对象。在这个例子中, getName()会获取对象并返回name属性的值。

结论

我会在哪里开始在哪里结束:我得承认我一开始对待weakmaps的态度是错的。我现在明白了为什么人们对于它会如此兴奋,如果我只是用它来创造真正私有的(和非hacky手段创造的)实例成员,我会觉得我只是得到了我工资的价值。我要感谢Nick Fitzgerald的文章,它启发了我写这篇文章,扩宽了我的视野,让我意识到了weakmaps的更多可能性。我可以很容易地预见到未来我将使用weakmaps作为我JavaScript的日常工具,我焦急地等待着我们可以跨浏览器使用它们的那一天。

参考文献

  1. Hiding implementation details with ECMAScript 6 WeakMaps by Nick Fitzgerald (fitzgeraldnick.com)

免责声明:任何本文所表达的观点和意见都属于Nicholas C. Zakas,不得以任何方式,映射到我的雇主,我的同事, Wrox出版社 , O'Reilly出版社 ,或其他任何人上。我只说我自己想说的,不代表他们。