[翻譯] 在JavaScript中使用weakmaps實現私有例項成員

zzNucker發表於2014-03-05

翻譯自 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出版社 ,或其他任何人上。我只說我自己想說的,不代表他們。

相關文章