基于JavaScript如何實(shí)現(xiàn)私有成員的語法特征及私有成員的實(shí)現(xiàn)方式
前言
在面向?qū)ο蟮木幊谭妒街?,封裝都是必不可少的一個(gè)概念,而在諸如 Java,C++等傳統(tǒng)的面向?qū)ο蟮恼Z言中, 私有成員是實(shí)現(xiàn)封裝的一個(gè)重要途徑。但在 JavaScript 中,確沒有在語法特性上對(duì)私有成員提供支持, 這也使得開發(fā)人員使出了各種奇技淫巧去實(shí)現(xiàn) JS 中的私有成員,以下將介紹下目前實(shí)現(xiàn) JS 私有成員特性的幾個(gè)方案以及它們之間的優(yōu)缺點(diǎn)對(duì)比。
現(xiàn)有的一些實(shí)現(xiàn)方案
約定命名方案
約定以下劃線'_'開頭的成員名作為私有成員,僅允許類成員方法訪問調(diào)用,外部不得訪問私有成員。簡(jiǎn)單的代碼如下:
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'
優(yōu)點(diǎn)
毫無疑問,約定命名是最簡(jiǎn)單的私有成員實(shí)現(xiàn)方案,沒有代碼層面上的工作。
調(diào)試方便,能夠在控制臺(tái)上直接看到對(duì)象上的私有成員,方便排查問題。
兼容性好,ie6+都支持
不足
無法阻止外部對(duì)私有成員的訪問和變更,如果真有不知道或者不遵守約定的開發(fā)人員變更私有屬性,也是無能為力。
必須強(qiáng)制或說服大家遵守這個(gè)約定,當(dāng)然這個(gè)在有代碼規(guī)范的團(tuán)隊(duì)中不是什么太大的問題。
es6 symbol 方案
在 es6中,引入了一個(gè) Symbol 的特性,該特性正是為了實(shí)現(xiàn)私有成員而引入的。
主要的思路是,為每一個(gè)私有成員的名稱產(chǎn)生一個(gè)隨機(jī)且唯一的字符串key,這個(gè) key 對(duì)外不可見,對(duì)內(nèi)的可見性則是通過 js 的閉包變量實(shí)現(xiàn),示例代碼如下:
JavaScript
(function() { var privateProp = Symbol(); // 每次調(diào)用會(huì)產(chǎn)生一個(gè)唯一的key function MyClass() { this[privateProp] = ‘privateProp'; // 閉包內(nèi)引用到這個(gè) key } MyClass.prototype.getPrivateProp = function () { return this[privateProp]; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,因?yàn)槌蓡T的key其實(shí)是隨機(jī)字符串
優(yōu)點(diǎn)
彌補(bǔ)了命名約定方案的缺陷,外部無法通過正常途徑獲得私有成員的訪問權(quán)。
調(diào)試便捷程度上可以接受,一般是通過給 symbol 的構(gòu)造函數(shù)傳入一個(gè)字符串參數(shù),則控制臺(tái)上對(duì)應(yīng)的私有屬性名稱會(huì)展示為:Symbol(key)
兼容性不錯(cuò),不支持 Symbol的瀏覽器可以很容易的 shim 出來。
不足
寫法上稍顯別扭,必須為每一個(gè)私有成員都創(chuàng)建一個(gè)閉包變量讓內(nèi)部方法可以訪問。
外部還是可以通過 Object.getOwnPropertySymbols的方式獲取實(shí)例的 symbol 屬性名稱,通過該名稱獲得私有成員的訪問權(quán)。這種場(chǎng)景出現(xiàn)得比較少,且知道這種途徑的開發(fā)人員水平相信都是有足夠的能力知道自己的行為會(huì)有什么影響,因此這個(gè)不足點(diǎn)也算不上真正意義的不足。
es6 WeakMap 方案
在 es6 中引入了 Map, WeakMap 容器,最大的特點(diǎn)是容器的鍵名可以是任意的數(shù)據(jù)類型,雖說初衷不是為了實(shí)現(xiàn)私有成員引入,但意外的可以被用來實(shí)現(xiàn)私有成員特性。
主要的思路是,在類的級(jí)別上創(chuàng)建一個(gè) WeakMap 容器,用于存儲(chǔ)各個(gè)實(shí)例的私有成員,這個(gè)容器對(duì)外不可見,對(duì)內(nèi)通過閉包方式可見;內(nèi)部方法通過將實(shí)例作為鍵名獲取容器上對(duì)應(yīng)實(shí)例的私有成員,示例代碼如下:
JavaScript
(function() { var privateStore = new WeakMap(); // 私有成員存儲(chǔ)容器 function MyClass() { privateStore.set(this, {privateProp: ‘privateProp'}); // 閉包內(nèi)引用到privateStore, 用當(dāng)前實(shí)例做 key,設(shè)置私有成員 } MyClass.prototype.getPrivateProp = function () { return privateStore.get(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,實(shí)例上并沒有 privateProp 屬性
優(yōu)點(diǎn)
彌補(bǔ)了命名約定方案的缺陷,外部無法通過正常途徑獲得私有成員的訪問權(quán)。
對(duì) WeakMap 做一些封裝,抽出一個(gè)私有特性的實(shí)現(xiàn)模塊,可以在寫法上相對(duì) Symbol 方案更加簡(jiǎn)潔干凈,其中一種封裝的實(shí)現(xiàn)可以查看參考文章3。
最后一個(gè)是個(gè)人認(rèn)為最大的優(yōu)勢(shì):基于 WeakMap 方案,可以方便的實(shí)現(xiàn)保護(hù)成員特性(這個(gè)話題會(huì)在其他文章說到:))
不足
不好調(diào)試,因?yàn)槭撬接谐蓡T都在閉包容器內(nèi),無法在控制臺(tái)打印實(shí)例查看對(duì)應(yīng)的私有成員
待確認(rèn)的性能問題,根據(jù) es6的相關(guān)郵件列表,weakmap 內(nèi)部似乎是通過順序一一對(duì)比的方式去定位 key 的,時(shí)間復(fù)雜度為 O(n),和 hash 算法的 O(1)相比會(huì)慢不少
最大的缺陷則是兼容性帶來的內(nèi)存膨脹問題,在不支持 WeakMap 的瀏覽器中是無法實(shí)現(xiàn) WeakMap 的弱引用特性,因此實(shí)例無法被垃圾回收。 比如示例代碼中 privateProp 是一個(gè)很大的數(shù)據(jù)項(xiàng),無弱引用的情況下,實(shí)例無法回收,從而造成內(nèi)存泄露。
現(xiàn)有實(shí)現(xiàn)方案小結(jié)
從上面的對(duì)比來看,Symbol方案最大優(yōu)勢(shì)在于很容易模擬實(shí)現(xiàn);而WeakMap的優(yōu)勢(shì)則是能夠?qū)崿F(xiàn)保護(hù)成員, 現(xiàn)階段無法忍受的不足是無法模擬實(shí)現(xiàn)弱引用特性而導(dǎo)致的內(nèi)存問題。于是我的思路又轉(zhuǎn)向了將兩者優(yōu)勢(shì)結(jié)合起來的方向。
Symbol + 類WeakMap 的整合方案
在 WeakMap 的方案中最大的問題是無法 shim 弱引用,較次要的問題是不大方便調(diào)試。
shim 出來的 WeakMap 主要是無法追溯實(shí)例的生命周期,而實(shí)例上的私有成員的生命周期又是依賴實(shí)例, 因此將實(shí)例級(jí)別的私有成員部分放在實(shí)例上不就好了? 實(shí)例沒了,自然其屬性也隨之摧毀。而私有存儲(chǔ)區(qū)域的隱藏則可以使用 Symol 來做。
該方案的提供一個(gè) createPrivate 函數(shù),該函數(shù)會(huì)返回一個(gè)私有的 token 函數(shù),對(duì)外不可見,對(duì)內(nèi)通過閉包函數(shù)獲得, 傳入當(dāng)前實(shí)例會(huì)返回當(dāng)前實(shí)例的私有存儲(chǔ)區(qū)域。使用方式如下:
JavaScript
(function() { var $private = createPrivate(); // 私有成員 token 函數(shù),可以傳入對(duì)象參數(shù),會(huì)作為原型鏈上的私有成員 function MyClass() { $private(this).privateProp = ‘privateProp' ; // 閉包內(nèi)引用到privateStore, 用當(dāng)前實(shí)例做 key,設(shè)置私有成員 } MyClass.prototype.getPrivateProp = function () { return $private(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 彈出 undefined,實(shí)例上并沒有 privateProp 屬性
代碼中主要就是實(shí)現(xiàn) createPrivate 函數(shù),大概的實(shí)現(xiàn)如下:
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]; }; }
上述實(shí)現(xiàn)做了兩層存儲(chǔ),privateStore 這層是實(shí)例上統(tǒng)一的私有成員存儲(chǔ)區(qū)域,而 classToken 對(duì)應(yīng)的則是繼承層次之間不同類的私有成員定義,基類有基類的私有成員區(qū)域,子類和基類的私有成員區(qū)域是不同的。
當(dāng)然,只做一層的存儲(chǔ)也可以實(shí)現(xiàn),兩層存儲(chǔ)僅僅是為了調(diào)試方便,可以直接在控制臺(tái)通過Symbol(‘privateStore')這個(gè)屬性來查看實(shí)例各個(gè)層次的私有部分。
奇葩的 es5 property getter 攔截方案
該方案純粹是閑得無聊玩了玩,主要是利用了 es5 提供的 getter,根據(jù) argument.callee.caller 去判斷調(diào)用場(chǎng)景,如果是外部的則拋異?;蚍祷?undefined, 如果是內(nèi)部調(diào)用則返回真正的私有成員,實(shí)現(xiàn)起來比較復(fù)雜,且不支持 strict 模式,不推薦使用。 有興趣的同學(xué)可以看看實(shí)現(xiàn)。
總結(jié)
以上幾個(gè)方案對(duì)比下來,我個(gè)人是傾向 Symbol+WeakMap 的整合方案,結(jié)合了兩者的優(yōu)點(diǎn),又彌補(bǔ)了 WeakMap 的不足和 Symbol 書寫的冗余。 當(dāng)然了,我相信隨著 JS 的發(fā)展,私有成員和保護(hù)成員也遲早會(huì)在語法層面上進(jìn)行支持,正如 es6 對(duì) class 關(guān)鍵字和 super 語法糖的支持一樣, 只是現(xiàn)階段需要開發(fā)者使用一些技巧去填補(bǔ)語言特性上的空白。
Javascript私有成員的實(shí)現(xiàn)方式
總體來講這本書還是可以的,但看完這本書還留了幾個(gè)問題一直困擾著我,如js中私有變量的實(shí)現(xiàn),prototype等,經(jīng)過自己一系列測(cè)試,現(xiàn)在終于弄明白了。
很多書上都是說,Javascript是不能真正實(shí)現(xiàn)Javascript私有成員的,因此在開發(fā)的時(shí)候,統(tǒng)一約定 __ 兩個(gè)下劃線開頭為私有變量。
后來,發(fā)現(xiàn)Javascript中閉包的特性,從而徹底解決了Javascript私有成員的問題。
function testFn(){ var _Name;//定義Javascript私有成員 this.setName = function(name){ _Name = name; //從當(dāng)前執(zhí)行環(huán)境中獲取_Name } this.getName = function(){ return _Name; } }// End testFn var test = testFn(); alert(typeof test._Name === "undefined")//true test.setName("KenChen");
test._Name 根本訪問不到,但是用對(duì)象方法能訪問到,因?yàn)殚]包能從當(dāng)前的執(zhí)行環(huán)境中獲取信息。
接下來我們看看,共有成員是怎樣實(shí)現(xiàn)的
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
接下來在看看類靜態(tài)變量是怎樣實(shí)現(xiàn)的
function testFn(){ } testFn.Name = "KenChen"; alert(testFn.Name);//KenChen testFn.Name = "CC"; alert(testFn.Name);//CC
相關(guān)文章
avalonjs實(shí)現(xiàn)仿微博的圖片拖動(dòng)特效
JavaScript實(shí)現(xiàn)仿微博的圖片拖動(dòng)特效,貌似這些天有不少朋友需要這功能,今天發(fā)現(xiàn)這款是js制作的好,不敢獨(dú)享,希望需要的朋友喜歡哦。2015-05-05用js判斷頁(yè)面刷新或關(guān)閉的方法(onbeforeunload與onunload事件)
Onunload,onbeforeunload都是在刷新或關(guān)閉時(shí)調(diào)用,可以在<script>腳本中通過window.onunload來指定或者在<body>里指定2012-06-06微信小程序?qū)崿F(xiàn)轉(zhuǎn)盤抽獎(jiǎng)
這篇文章主要為大家詳細(xì)介紹了微信小程序?qū)崿F(xiàn)轉(zhuǎn)盤抽獎(jiǎng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-09-09JavaScript使用?for...in?、?for...of?或者?forEach()?遍歷元素的示例
for...in?、?for...of?和?forEach?都是用于循環(huán)遍歷集合元素的方法,但它們之間有一些重要的區(qū)別,本文通過實(shí)例代碼介紹JavaScript使用?for...in?、?for...of?或者?forEach()?遍歷元素的相關(guān)知識(shí),感興趣的朋友一起看看吧2023-09-09基于Bootstrap仿淘寶分頁(yè)控件實(shí)現(xiàn)代碼
這篇文章主要介紹了基于Bootstrap仿淘寶分頁(yè)控件實(shí)現(xiàn)代碼,本文分步驟給大家介紹的非常詳細(xì),感興趣的朋友參考下吧2016-11-11