如何在 JavaScript 物件中嵌入私有成員

oschina發表於2015-07-04

最近,我開發一個專案 Angular Cloud Data Connector, 幫助Angular開發者使用雲資料,特別是 Azure移動服務, 使用WEB標準,像索引資料庫(indexed DB)。我嘗試建立一種方式,使得JavaScript開發者能將私有成員嵌入到一個物件中。

我解決這個問題的技術用到了我命名的閉包空間(closure space)。在這篇入門文章中,我要分享的是如何在你的專案中用它,及它對主流瀏覽器的效能和記憶體的影響。

在深入學習前,我們們先說下,你為什麼需要用到私有成員(private members), 還有一種替代方式來模擬私有成員。

如果你想點評本文,盡情推(twitter)我: @deltakosh

1. 為何要用私有成員(Private Members)

當你用JavaScript 建立一個物件時,可以宣告值成員(value members)。 如果你打算控制對它們的讀/寫訪問操作,可以如下宣告:

var entity = {};

entity._property = "hello world";
Object.defineProperty(entity, "property", {
    get: function () { return this._property; },
    set: function (value) {
        this._property = value;
    },
    enumerable: true,
    configurable: true
});

這樣實現,你能完全控制讀和寫操作。問題在於_property 成員仍然可以直接訪問和修改。

這也就是為何我們需要更加穩定可靠的方式,宣告私有成員,它智慧通過物件的方法來訪問。

2. 使用閉包空間(Closure Space)

解決方法是使用閉包空間。每當內部函式 (inner fanction) 訪問來自外部函式作用域的變數時,瀏覽器為你分配一段記憶體空間。有時很取巧,不過就我們的題目來講,這算是一個完美的解決方案。

我們在上個程式碼版本中新增這個特性:
var createProperty = function (obj, prop, currentValue) 
{
    Object.defineProperty(obj, prop, 
    {
            get: function () { return currentValue; },
            set: function (value) {
            currentValue = value;
                    },
                    enumerable: true,
                    configurable: true    });
                    } 
var entity = {}; 
var myVar = "hello world";createProperty(entity, "property", myVar);

示例中,createProperty 函式有一個 currentValue 變數,存在 get 和 set 方法。此變數會儲存到 get 和 set 函式的閉包空間中。現在,只有這兩個函式能看到和更新 currentValue 變數! 任務完成!

唯一需要警惕 caveat,警告,注意)的是源值 (myVar) 仍可訪問。下面給出另一個更健壯的版本(保護 myVar 變數):

var createProperty = function (obj, prop) {
    var currentValue = obj[prop];
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

var entity = {
    property: "hello world"
};

createProperty(entity, "property");

採用該函式, 即便源值都銷燬(destructed,注:意思是不能直接賦值)了。到此大功告成了!

3. 效能考慮Performance Considerations

現在我們們看看效能。

很明顯,比起一個簡單的變數,閉包空間,甚或(物件)屬性要慢的多,且更消耗資源。這就是本文更多關注普通方式和閉包空間機制差異的原因。

為證明閉包空間機制並不比標準方式更消耗資源, 我寫了下面程式碼做個基準測試:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
</head>
<style>
    html {
        font-family: "Helvetica Neue", Helvetica;
    }
</style>
<body>
    <div id="results">Computing...</div>
    <script>
        var results = document.getElementById("results");
        var sampleSize = 1000000;
        var opCounts = 1000000;

        var entities = [];

        setTimeout(function () {
            // Creating entities
            for (var index = 0; index < sampleSize; index++) {
                entities.push({
                    property: "hello world (" + index + ")"
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML = "<strong>Results:</strong><br>Using member access: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Closure space =======================================
            var createProperty = function (obj, prop, currentValue) {
                Object.defineProperty(obj, prop, {
                    get: function () { return currentValue; },
                    set: function (value) {
                        currentValue = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }
            // Adding property and using closure space to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                var currentValue = entity.property;
                createProperty(entity, "property", currentValue);
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using closure space: <strong>" + (end - start) + "</strong> ms";
        }, 0);

        setTimeout(function () {
            // Using local member =======================================
            // Adding property and using local member to save private value
            for (var index = 0; index < sampleSize; index++) {
                var entity = entities[index];

                entity._property = entity.property;
                Object.defineProperty(entity, "property", {
                    get: function () { return this._property; },
                    set: function (value) {
                        this._property = value;
                    },
                    enumerable: true,
                    configurable: true
                });
            }

            // Random reads
            var start = new Date().getTime();
            for (index = 0; index < opCounts; index++) {
                var position = Math.floor(Math.random() * entities.length);
                var temp = entities[position].property;
            }
            var end = new Date().getTime();

            results.innerHTML += "<br>Using local member: <strong>" + (end - start) + "</strong> ms";
        }, 0);

    </script>
</body>
</html>

我建立了一百萬個物件,都有屬性成員。要完成下面三個測試:

  • 執行 1百萬次隨機訪問屬性。
  • 執行1百萬次隨機訪問閉包空間實現版本。
  • 執行1百萬次隨機訪問常規get/set實現版本。

測試結果參見下面表格和圖表:

JavaScript:如何在物件中嵌入私有成員JavaScript:如何在物件中嵌入私有成員

我們發現,閉包空間實現總是快於常規實現,根據瀏覽器的不同,還可以做進一步的效能優化。

Chrome 上的效能表現低於預期。或許存在 bug,因此,為確認(存在 bug),我聯絡了 Google 專案組,描述發生的症狀。還有,如果你打算測試在 Microsoft Edge —微軟新發布的瀏覽器,在windows10 中預設安裝—中的效能表現,你可以點選下載 

然而,如果仔細研究,你會發現,使用閉包空間或屬性比直接訪問變數成員要10倍左右。 因此,使用要恰當且謹慎。

JavaScript:如何在物件中嵌入私有成員

4. 記憶體佔用(Memory Footprint)

我們也得驗證該技術不會消耗過多記憶體。為測試記憶體佔用基準情況,我寫了下面程式碼段:

直接屬性引用版本(Reference Code)

var sampleSize = 1000000;
 var entities = []; 
// Creating entities
for (var index = 0; index < sampleSize; index++) {
    entities.push({
            property: "hello world (" + index + ")"
});}

常規方式版本(Regular Way,get/set)

var sampleSize = 1000000;

var entities = [];

// Adding property and using local member to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    entity._property = "hello world (" + index + ")";
    Object.defineProperty(entity, "property", {
        get: function () { return this._property; },
        set: function (value) {
            this._property = value;
        },
        enumerable: true,
        configurable: true
    });

    entities.push(entity);
}

閉包空間版本(Closure Space Version)

var sampleSize = 1000000;

var entities = [];

var createProperty = function (obj, prop, currentValue) {
    Object.defineProperty(obj, prop, {
        get: function () { return currentValue; },
        set: function (value) {
            currentValue = value;
        },
        enumerable: true,
        configurable: true
    });
}

// Adding property and using closure space to save private value
for (var index = 0; index < sampleSize; index++) {
    var entity = {};

    var currentValue = "hello world (" + index + ")";
    createProperty(entity, "property", currentValue);

    entities.push(entity);
}

之後,我(在三個主流瀏覽器上)執行所有的三段程式碼,啟動(瀏覽器)內嵌的記憶體效能分析器(本示例中使用 F12 工具條):

JavaScript:如何在物件中嵌入私有成員

我計算機上執行的結果如下圖表:

JavaScript:如何在物件中嵌入私有成員

就閉包空間和常規方式,只有 Chrome上,閉包空間(記憶體佔用)表現稍好,在 IE11 和 Firefox上佔用記憶體反而增多,但是瀏覽器的比較結果e—對於現代瀏覽器,使用者很可能不會在意這點差別。

更多 JavaScript 實踐

或許你會吃驚,微軟提供了一批有關開源 Javascript 主題的免費學習材料, 我們正在發起一個任務,關於建立更多 Microsoft Edge 來臨 系列。 檢視我的文章:

或者我們團隊系列:

以及一些免費工具:Visual Studio 社群Azure 試用版跨瀏覽器測試工具用於 Mac, Linux, 或者 Windows。

結論(Conclusion)

如你所見,對於建立真正的私有資料來講,閉包空間屬性(機制)是一個很棒的做法。或許你得面對記憶體消耗小幅度增加(問題),但就我的看法,這卻很合理 (這個代價可以換取相對於常規方法更高的效能增長)。

隨帶說一句, 如果你要自己動手試試,所以程式碼可以在 here下載。 推薦一篇不錯的文章, “how-to” on Azure Mobile Services here

相關文章