前言
在面向對象的編程范式中,封裝都是必不可少的一個概念,而在諸如 Java,C++等傳統的面向對象的語言中, 私有成員是實現封裝的一個重要途徑。但在 JavaScript 中,確沒有在語法特性上對私有成員提供支持, 這也使得開發人員使出了各種奇技淫巧去實現 JS 中的私有成員,以下將介紹下目前實現 JS 私有成員特性的幾個方案以及它們之間的優缺點對比。
現有的一些實現方案
約定命名方案
約定以下劃線'_'開頭的成員名作為私有成員,僅允許類成員方法訪問調用,外部不得訪問私有成員。簡單的代碼如下:
JavaScript
var MyClass = function () { this._privateProp = ‘privateProp'; }; MyClass.prototype.getPrivateProp = function () { return this._privateProp; }; var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my._privateProp); // 並未真正隱藏,依然彈出 ‘privateProp'
優點
毫無疑問,約定命名是最簡單的私有成員實現方案,沒有代碼層面上的工作。
調試方便,能夠在控制台上直接看到對象上的私有成員,方便排查問題。
兼容性好,ie6+都支持
不足
無法阻止外部對私有成員的訪問和變更,如果真有不知道或者不遵守約定的開發人員變更私有屬性,也是無能為力。
必須強制或說服大家遵守這個約定,當然這個在有代碼規范的團隊中不是什麼太大的問題。
es6 symbol 方案
在 es6中,引入了一個 Symbol 的特性,該特性正是為了實現私有成員而引入的。
主要的思路是,為每一個私有成員的名稱產生一個隨機且唯一的字符串key,這個 key 對外不可見,對內的可見性則是通過 js 的閉包變量實現,示例代碼如下:
JavaScript
(function() { var privateProp = Symbol(); // 每次調用會產生一個唯一的key function MyClass() { this[privateProp] = ‘privateProp'; // 閉包內引用到這個 key } MyClass.prototype.getPrivateProp = function () { return this[privateProp]; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,因為成員的key其實是隨機字符串
優點
彌補了命名約定方案的缺陷,外部無法通過正常途徑獲得私有成員的訪問權。
調試便捷程度上可以接受,一般是通過給 symbol 的構造函數傳入一個字符串參數,則控制台上對應的私有屬性名稱會展示為:Symbol(key)
兼容性不錯,不支持 Symbol的浏覽器可以很容易的 shim 出來。
不足
寫法上稍顯別扭,必須為每一個私有成員都創建一個閉包變量讓內部方法可以訪問。
外部還是可以通過 Object.getOwnPropertySymbols的方式獲取實例的 symbol 屬性名稱,通過該名稱獲得私有成員的訪問權。這種場景出現得比較少,且知道這種途徑的開發人員水平相信都是有足夠的能力知道自己的行為會有什麼影響,因此這個不足點也算不上真正意義的不足。
es6 WeakMap 方案
在 es6 中引入了 Map, WeakMap 容器,最大的特點是容器的鍵名可以是任意的數據類型,雖說初衷不是為了實現私有成員引入,但意外的可以被用來實現私有成員特性。
主要的思路是,在類的級別上創建一個 WeakMap 容器,用於存儲各個實例的私有成員,這個容器對外不可見,對內通過閉包方式可見;內部方法通過將實例作為鍵名獲取容器上對應實例的私有成員,示例代碼如下:
JavaScript
(function() { var privateStore = new WeakMap(); // 私有成員存儲容器 function MyClass() { privateStore.set(this, {privateProp: ‘privateProp'}); // 閉包內引用到privateStore, 用當前實例做 key,設置私有成員 } MyClass.prototype.getPrivateProp = function () { return privateStore.get(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,實例上並沒有 privateProp 屬性
優點
彌補了命名約定方案的缺陷,外部無法通過正常途徑獲得私有成員的訪問權。
對 WeakMap 做一些封裝,抽出一個私有特性的實現模塊,可以在寫法上相對 Symbol 方案更加簡潔干淨,其中一種封裝的實現可以查看參考文章3。
最後一個是個人認為最大的優勢:基於 WeakMap 方案,可以方便的實現保護成員特性(這個話題會在其他文章說到:))
不足
不好調試,因為是私有成員都在閉包容器內,無法在控制台打印實例查看對應的私有成員
待確認的性能問題,根據 es6的相關郵件列表,weakmap 內部似乎是通過順序一一對比的方式去定位 key 的,時間復雜度為 O(n),和 hash 算法的 O(1)相比會慢不少
最大的缺陷則是兼容性帶來的內存膨脹問題,在不支持 WeakMap 的浏覽器中是無法實現 WeakMap 的弱引用特性,因此實例無法被垃圾回收。 比如示例代碼中 privateProp 是一個很大的數據項,無弱引用的情況下,實例無法回收,從而造成內存洩露。
現有實現方案小結
從上面的對比來看,Symbol方案最大優勢在於很容易模擬實現;而WeakMap的優勢則是能夠實現保護成員, 現階段無法忍受的不足是無法模擬實現弱引用特性而導致的內存問題。於是我的思路又轉向了將兩者優勢結合起來的方向。
Symbol + 類WeakMap 的整合方案
在 WeakMap 的方案中最大的問題是無法 shim 弱引用,較次要的問題是不大方便調試。
shim 出來的 WeakMap 主要是無法追溯實例的生命周期,而實例上的私有成員的生命周期又是依賴實例, 因此將實例級別的私有成員部分放在實例上不就好了? 實例沒了,自然其屬性也隨之摧毀。而私有存儲區域的隱藏則可以使用 Symol 來做。
該方案的提供一個 createPrivate 函數,該函數會返回一個私有的 token 函數,對外不可見,對內通過閉包函數獲得, 傳入當前實例會返回當前實例的私有存儲區域。使用方式如下:
JavaScript
(function() { var $private = createPrivate(); // 私有成員 token 函數,可以傳入對象參數,會作為原型鏈上的私有成員 function MyClass() { $private(this).privateProp = ‘privateProp' ; // 閉包內引用到privateStore, 用當前實例做 key,設置私有成員 } MyClass.prototype.getPrivateProp = function () { return $private(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,實例上並沒有 privateProp 屬性
代碼中主要就是實現 createPrivate 函數,大概的實現如下:
JavaScript
// createPrivate.js function createPrivate(prototype) { var privateStore = Symbol('privateStore'); var classToken = Symbol(‘classToken'); return function getPrivate(instance) { if (!instance.hasOwnProperty(privateStore)) { instance[privateStore] = {}; } var store = instance[classToken]; store[token] = store[token] || Object.create(prototype || {}); return store[token]; }; }
上述實現做了兩層存儲,privateStore 這層是實例上統一的私有成員存儲區域,而 classToken 對應的則是繼承層次之間不同類的私有成員定義,基類有基類的私有成員區域,子類和基類的私有成員區域是不同的。
當然,只做一層的存儲也可以實現,兩層存儲僅僅是為了調試方便,可以直接在控制台通過Symbol(‘privateStore')這個屬性來查看實例各個層次的私有部分。
奇葩的 es5 property getter 攔截方案
該方案純粹是閒得無聊玩了玩,主要是利用了 es5 提供的 getter,根據 argument.callee.caller 去判斷調用場景,如果是外部的則拋異常或返回 undefined, 如果是內部調用則返回真正的私有成員,實現起來比較復雜,且不支持 strict 模式,不推薦使用。 有興趣的同學可以看看實現。
總結
以上幾個方案對比下來,我個人是傾向 Symbol+WeakMap 的整合方案,結合了兩者的優點,又彌補了 WeakMap 的不足和 Symbol 書寫的冗余。 當然了,我相信隨著 JS 的發展,私有成員和保護成員也遲早會在語法層面上進行支持,正如 es6 對 class 關鍵字和 super 語法糖的支持一樣, 只是現階段需要開發者使用一些技巧去填補語言特性上的空白。
Javascript私有成員的實現方式
總體來講這本書還是可以的,但看完這本書還留了幾個問題一直困擾著我,如js中私有變量的實現,prototype等,經過自己一系列測試,現在終於弄明白了。
很多書上都是說,Javascript是不能真正實現Javascript私有成員的,因此在開發的時候,統一約定 __ 兩個下劃線開頭為私有變量。
後來,發現Javascript中閉包的特性,從而徹底解決了Javascript私有成員的問題。
function testFn(){ var _Name;//定義Javascript私有成員 this.setName = function(name){ _Name = name; //從當前執行環境中獲取_Name } this.getName = function(){ return _Name; } }// End testFn var test = testFn(); alert(typeof test._Name === "undefined")//true test.setName("KenChen");
test._Name 根本訪問不到,但是用對象方法能訪問到,因為閉包能從當前的執行環境中獲取信息。
接下來我們看看,共有成員是怎樣實現的
function testFn(name){ this.Name = name; this.getName = function(){ return this.Name; } } var test = new testFn("KenChen"); test.getName(); //KenChen test.Name = "CC"; est.getName();//CC
接下來在看看類靜態變量是怎樣實現的
function testFn(){ } testFn.Name = "KenChen"; alert(testFn.Name);//KenChen testFn.Name = "CC"; alert(testFn.Name);//CC