如何在 JavaScript 物件中嵌入私有成員
最近,我開發一個專案 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實現版本。
測試結果參見下面表格和圖表:
我們發現,閉包空間實現總是快於常規實現,根據瀏覽器的不同,還可以做進一步的效能優化。
Chrome 上的效能表現低於預期。或許存在 bug,因此,為確認(存在 bug),我聯絡了 Google 專案組,描述發生的症狀。還有,如果你打算測試在 Microsoft Edge —微軟新發布的瀏覽器,在windows10 中預設安裝—中的效能表現,你可以點選下載 。
然而,如果仔細研究,你會發現,使用閉包空間或屬性比直接訪問變數成員要10倍左右。 因此,使用要恰當且謹慎。
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 工具條):
我計算機上執行的結果如下圖表:
就閉包空間和常規方式,只有 Chrome上,閉包空間(記憶體佔用)表現稍好,在 IE11 和 Firefox上佔用記憶體反而增多,但是瀏覽器的比較結果e—對於現代瀏覽器,使用者很可能不會在意這點差別。
更多 JavaScript 實踐
或許你會吃驚,微軟提供了一批有關開源 Javascript 主題的免費學習材料, 我們正在發起一個任務,關於建立更多 Microsoft Edge 來臨 系列。 檢視我的文章:
- 基於 HTML5 和 Babylon.JS 開發 WebGL 3D 基礎
- 構建單頁面應用,基於 ASP.NET 和 AngularJS
- HTML 高階影像技術
或者我們團隊系列:
- HTML/JavaScript 效能優化使用技巧 (該系列有7部分,從響應式設計到休閒遊戲的效能優化)
- 現代 Web 平臺快速起步 ( HTML, CSS, and JS基礎)
- 開發通用的 Windows Apps,使用 HTML 和 JavaScript 快速起步 (使用你自己的JS構建app)
以及一些免費工具:Visual Studio 社群,Azure 試用版和跨瀏覽器測試工具用於 Mac, Linux, 或者 Windows。
結論(Conclusion)
如你所見,對於建立真正的私有資料來講,閉包空間屬性(機制)是一個很棒的做法。或許你得面對記憶體消耗小幅度增加(問題),但就我的看法,這卻很合理 (這個代價可以換取相對於常規方法更高的效能增長)。
隨帶說一句, 如果你要自己動手試試,所以程式碼可以在 here下載。 推薦一篇不錯的文章, “how-to” on Azure Mobile Services here。
相關文章
- JavaScript 的私有成員JavaScript
- 在 JavaScript 中實現私有成員的語法特性JavaScript
- 微課|中學生可以這樣學Python(7.3.1節):私有成員與公有成員Python
- ⦁ 類的私有成員
- 『無為則無心』Python物件導向 — 55、多層繼承和繼承中的私有成員Python物件繼承
- JavaScript之坑了我--閉包助力OOP之模擬私有成員屬性JavaScriptOOP
- C++ 突破私有成員訪問限制C++
- C/C++—— 除了用類成員函式訪問類私有成員變數外,還可以通過類物件地址來直接訪問和修改類的私有成員變數C++函式變數物件
- javascript中隱私型別轉換JavaScript型別
- 如何在batch指令碼中嵌入python程式碼BAT指令碼Python
- JavaScript中的Array物件JavaScript物件
- C++中的成員物件C++物件
- Observable:Markdown中可嵌入JavaScript的NotebookJavaScript
- 如何在現有的Vue專案中嵌入 Blazor專案?VueBlazor
- JavaScript中物件的拷貝JavaScript物件
- JavaScript中 Map 物件詳解JavaScript物件
- 在 JavaScript 中如何克隆物件?JavaScript物件
- 教你如何在Golang中執行JavaScriptGolangJavaScript
- 如何在java類中呼叫websphere中的jndi物件JavaWeb物件
- 『無為則無心』Python物件導向 — 51、私有成員變數(類中資料的封裝)Python物件變數封裝
- 深入瞭解JavaScript中的物件JavaScript物件
- 談談JavaScript中建立物件(Object)JavaScript物件Object
- Javascript中的陣列物件排序JavaScript陣列物件排序
- JavaScript 中物件的深拷貝JavaScript物件
- JavaScript中的物件導向----類JavaScript物件
- 詳解Javascript中的Object物件JavaScriptObject物件
- 在JavaScript中建立新物件(轉)JavaScript物件
- 詳解JavaScript中的嵌入式資料庫JavaScript資料庫
- 如何在 JavaScript 中更好地使用陣列JavaScript陣列
- javascript如何在元素中插入新的元素JavaScript
- 對JavaScript中函式物件的理解JavaScript函式物件
- 聊一聊Javascript中的Promise物件JavaScriptPromise物件
- 瞭解 JavaScript 中的內建物件JavaScript物件
- JavaScript 中的 Range 和 Selection 物件JavaScript物件
- JPA實體中欄位對映補充和嵌入物件物件
- 如何在JSP頁面中傳遞類物件JS物件
- Javascript 物件 – 字串物件JavaScript物件字串
- CoffeeScript攻略1.1:嵌入JavaScriptJavaScript