如何在 JavaScript 對象中嵌入私有成員
最近,我開發(fā)一個項目 Angular Cloud Data Connector, 幫助Angular開發(fā)者使用云數(shù)據(jù),特別是 Azure移動服務(wù), 使用WEB標(biāo)準(zhǔn),像索引數(shù)據(jù)庫(indexed DB)。我嘗試建立一種方式,使得JavaScript開發(fā)者能將私有成員嵌入到一個對象中。
我解決這個問題的技術(shù)用到了我命名的閉包空間(closure space)。在這篇入門文章中,我要分享的是如何在你的項目中用它,及它對主流瀏覽器的性能和內(nèi)存的影響。
在深入學(xué)習(xí)前,咱們先說下,你為什么需要用到私有成員(private members), 還有一種替代方式來模擬私有成員。
如果你想點評本文,盡情推(twitter)我: @deltakosh。
1. 為何要用私有成員(Private Members)
當(dāng)你用JavaScript 創(chuàng)建一個對象時,可以聲明值成員(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
- });
這樣實現(xiàn),你能完全控制讀和寫操作。問題在于_property 成員仍然可以直接訪問和修改。
這也就是為何我們需要更加穩(wěn)定可靠的方式,聲明私有成員,它智能通過對象的方法來訪問。
#p#
2. 使用閉包空間(Closure Space)
解決方法是使用閉包空間。每當(dāng)內(nèi)部函數(shù) (inner fanction) 訪問來自外部函數(shù)作用域的變量時,瀏覽器為你分配一段內(nèi)存空間。有時很取巧,不過就我們的題目來講,這算是一個***的解決方案。
我們在上個代碼版本中添加這個特性:
- 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 函數(shù)有一個 currentValue 變量,存在 get 和 set 方法。此變量會保存到 get 和 set 函數(shù)的閉包空間中?,F(xiàn)在,只有這兩個函數(shù)能看到和更新 currentValue 變量! 任務(wù)完成!
唯一需要警惕 caveat,警告,注意)的是源值 (myVar) 仍可訪問。下面給出另一個更健壯的版本(保護(hù) 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");
采用該函數(shù), 即便源值都銷毀(destructed,注:意思是不能直接賦值)了。到此大功告成了!
#p#
3. 性能考慮Performance Considerations
現(xiàn)在咱們看看性能。
很明顯,比起一個簡單的變量,閉包空間,甚或(對象)屬性要慢的多,且更消耗資源。這就是本文更多關(guān)注普通方式和閉包空間機制差異的原因。
為證明閉包空間機制并不比標(biāo)準(zhǔn)方式更消耗資源, 我寫了下面代碼做個基準(zhǔn)測試:
- <!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>
我創(chuàng)建了一百萬個對象,都有屬性成員。要完成下面三個測試:
-
執(zhí)行 1百萬次隨機訪問屬性。
-
執(zhí)行1百萬次隨機訪問閉包空間實現(xiàn)版本。
-
執(zhí)行1百萬次隨機訪問常規(guī)get/set實現(xiàn)版本。
測試結(jié)果參見下面表格和圖表:
我們發(fā)現(xiàn),閉包空間實現(xiàn)總是快于常規(guī)實現(xiàn),根據(jù)瀏覽器的不同,還可以做進(jìn)一步的性能優(yōu)化。
Chrome 上的性能表現(xiàn)低于預(yù)期?;蛟S存在 bug,因此,為確認(rèn)(存在 bug),我聯(lián)系了 Google 項目組,描述發(fā)生的癥狀。還有,如果你打算測試在 Microsoft Edge —微軟新發(fā)布的瀏覽器,在windows10 中默認(rèn)安裝—中的性能表現(xiàn),你可以點擊下載 。
然而,如果仔細(xì)研究,你會發(fā)現(xiàn),使用閉包空間或?qū)傩员戎苯釉L問變量成員要10倍左右。 因此,使用要恰當(dāng)且謹(jǐn)慎。
#p#
4. 內(nèi)存占用(Memory Footprint)
我們也得驗證該技術(shù)不會消耗過多內(nèi)存。為測試內(nèi)存占用基準(zhǔn)情況,我寫了下面代碼段:
直接屬性引用版本(Reference Code)
- var sampleSize = 1000000;
- var entities = [];
- // Creating entities
- for (var index = 0; index < sampleSize; index++) {
- entities.push({
- property: "hello world (" + index + ")"
- });}
- 常規(guī)方式版本(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);
- }
之后,我(在三個主流瀏覽器上)運行所有的三段代碼,啟動(瀏覽器)內(nèi)嵌的內(nèi)存性能分析器(本示例中使用 F12 工具條):
我計算機上運行的結(jié)果如下圖表:
就閉包空間和常規(guī)方式,只有 Chrome上,閉包空間(內(nèi)存占用)表現(xiàn)稍好,在 IE11 和 Firefox上占用內(nèi)存反而增多,但是瀏覽器的比較結(jié)果e—對于現(xiàn)代瀏覽器,用戶很可能不會在意這點差別。
更多 JavaScript 實踐
或許你會吃驚,微軟提供了一批有關(guān)開源 Javascript 主題的免費學(xué)習(xí)材料, 我們正在發(fā)起一個任務(wù),關(guān)于創(chuàng)建更多 Microsoft Edge 來臨 系列。 查看我的文章:
-
基于 HTML5 和 Babylon.JS 開發(fā) WebGL 3D 基礎(chǔ)
或者我們團隊系列:
-
HTML/JavaScript 性能優(yōu)化使用技巧 (該系列有7部分,從響應(yīng)式設(shè)計到休閑游戲的性能優(yōu)化)
-
現(xiàn)代 Web 平臺快速起步 ( HTML, CSS, and JS基礎(chǔ))
-
開發(fā)通用的 Windows Apps,使用 HTML 和 JavaScript 快速起步 (使用你自己的JS構(gòu)建app)
以及一些免費工具:Visual Studio 社區(qū),Azure 試用版和跨瀏覽器測試工具用于 Mac, Linux, 或者 Windows。
結(jié)論(Conclusion)
如你所見,對于創(chuàng)建真正的私有數(shù)據(jù)來講,閉包空間屬性(機制)是一個很棒的做法?;蛟S你得面對內(nèi)存消耗小幅度增加(問題),但就我的看法,這卻很合理 (這個代價可以換取相對于常規(guī)方法更高的性能增長)。
隨帶說一句, 如果你要自己動手試試,所以代碼可以在 here下載。 推薦一篇不錯的文章, “how-to” on Azure Mobile Services here。