基于JavaScript如何實(shí)現(xiàn)私有成員的語(yǔ)法特征及私有成員的實(shí)現(xiàn)方式
前言
在面向?qū)ο蟮木幊谭妒街?,封裝都是必不可少的一個(gè)概念,而在諸如 Java,C++等傳統(tǒng)的面向?qū)ο蟮恼Z(yǔ)言中, 私有成員是實(shí)現(xiàn)封裝的一個(gè)重要途徑。但在 JavaScript 中,確沒(méi)有在語(yǔ)法特性上對(duì)私有成員提供支持, 這也使得開(kāi)發(fā)人員使出了各種奇技淫巧去實(shí)現(xiàn) JS 中的私有成員,以下將介紹下目前實(shí)現(xiàn) JS 私有成員特性的幾個(gè)方案以及它們之間的優(yōu)缺點(diǎn)對(duì)比。
現(xiàn)有的一些實(shí)現(xiàn)方案
約定命名方案
約定以下劃線'_'開(kāi)頭的成員名作為私有成員,僅允許類成員方法訪問(wèn)調(diào)用,外部不得訪問(wèn)私有成員。簡(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)
毫無(wú)疑問(wèn),約定命名是最簡(jiǎn)單的私有成員實(shí)現(xiàn)方案,沒(méi)有代碼層面上的工作。
調(diào)試方便,能夠在控制臺(tái)上直接看到對(duì)象上的私有成員,方便排查問(wèn)題。
兼容性好,ie6+都支持
不足
無(wú)法阻止外部對(duì)私有成員的訪問(wèn)和變更,如果真有不知道或者不遵守約定的開(kāi)發(fā)人員變更私有屬性,也是無(wú)能為力。
必須強(qiáng)制或說(shuō)服大家遵守這個(gè)約定,當(dāng)然這個(gè)在有代碼規(guī)范的團(tuán)隊(duì)中不是什么太大的問(wèn)題。
es6 symbol 方案
在 es6中,引入了一個(gè) Symbol 的特性,該特性正是為了實(shí)現(xiàn)私有成員而引入的。
主要的思路是,為每一個(gè)私有成員的名稱產(chǎn)生一個(gè)隨機(jī)且唯一的字符串key,這個(gè) key 對(duì)外不可見(jiàn),對(duì)內(nèi)的可見(jiàn)性則是通過(guò) 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ǔ)了命名約定方案的缺陷,外部無(wú)法通過(guò)正常途徑獲得私有成員的訪問(wèn)權(quán)。
調(diào)試便捷程度上可以接受,一般是通過(guò)給 symbol 的構(gòu)造函數(shù)傳入一個(gè)字符串參數(shù),則控制臺(tái)上對(duì)應(yīng)的私有屬性名稱會(huì)展示為:Symbol(key)
兼容性不錯(cuò),不支持 Symbol的瀏覽器可以很容易的 shim 出來(lái)。
不足
寫法上稍顯別扭,必須為每一個(gè)私有成員都創(chuàng)建一個(gè)閉包變量讓內(nèi)部方法可以訪問(wèn)。
外部還是可以通過(guò) Object.getOwnPropertySymbols的方式獲取實(shí)例的 symbol 屬性名稱,通過(guò)該名稱獲得私有成員的訪問(wèn)權(quán)。這種場(chǎng)景出現(xiàn)得比較少,且知道這種途徑的開(kāi)發(fā)人員水平相信都是有足夠的能力知道自己的行為會(huì)有什么影響,因此這個(gè)不足點(diǎn)也算不上真正意義的不足。
es6 WeakMap 方案
在 es6 中引入了 Map, WeakMap 容器,最大的特點(diǎn)是容器的鍵名可以是任意的數(shù)據(jù)類型,雖說(shuō)初衷不是為了實(shí)現(xiàn)私有成員引入,但意外的可以被用來(lái)實(shí)現(xiàn)私有成員特性。
主要的思路是,在類的級(jí)別上創(chuàng)建一個(gè) WeakMap 容器,用于存儲(chǔ)各個(gè)實(shí)例的私有成員,這個(gè)容器對(duì)外不可見(jiàn),對(duì)內(nèi)通過(guò)閉包方式可見(jiàn);內(nèi)部方法通過(guò)將實(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í)例上并沒(méi)有 privateProp 屬性
優(yōu)點(diǎn)
彌補(bǔ)了命名約定方案的缺陷,外部無(wú)法通過(guò)正常途徑獲得私有成員的訪問(wèn)權(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ì)在其他文章說(shuō)到:))
不足
不好調(diào)試,因?yàn)槭撬接谐蓡T都在閉包容器內(nèi),無(wú)法在控制臺(tái)打印實(shí)例查看對(duì)應(yīng)的私有成員
待確認(rèn)的性能問(wèn)題,根據(jù) es6的相關(guān)郵件列表,weakmap 內(nèi)部似乎是通過(guò)順序一一對(duì)比的方式去定位 key 的,時(shí)間復(fù)雜度為 O(n),和 hash 算法的 O(1)相比會(huì)慢不少
最大的缺陷則是兼容性帶來(lái)的內(nèi)存膨脹問(wèn)題,在不支持 WeakMap 的瀏覽器中是無(wú)法實(shí)現(xiàn) WeakMap 的弱引用特性,因此實(shí)例無(wú)法被垃圾回收。 比如示例代碼中 privateProp 是一個(gè)很大的數(shù)據(jù)項(xiàng),無(wú)弱引用的情況下,實(shí)例無(wú)法回收,從而造成內(nèi)存泄露。
現(xiàn)有實(shí)現(xiàn)方案小結(jié)
從上面的對(duì)比來(lái)看,Symbol方案最大優(yōu)勢(shì)在于很容易模擬實(shí)現(xiàn);而WeakMap的優(yōu)勢(shì)則是能夠?qū)崿F(xiàn)保護(hù)成員, 現(xiàn)階段無(wú)法忍受的不足是無(wú)法模擬實(shí)現(xiàn)弱引用特性而導(dǎo)致的內(nèi)存問(wèn)題。于是我的思路又轉(zhuǎn)向了將兩者優(yōu)勢(shì)結(jié)合起來(lái)的方向。
Symbol + 類WeakMap 的整合方案
在 WeakMap 的方案中最大的問(wèn)題是無(wú)法 shim 弱引用,較次要的問(wèn)題是不大方便調(diào)試。
shim 出來(lái)的 WeakMap 主要是無(wú)法追溯實(shí)例的生命周期,而實(shí)例上的私有成員的生命周期又是依賴實(shí)例, 因此將實(shí)例級(jí)別的私有成員部分放在實(shí)例上不就好了? 實(shí)例沒(méi)了,自然其屬性也隨之摧毀。而私有存儲(chǔ)區(qū)域的隱藏則可以使用 Symol 來(lái)做。
該方案的提供一個(gè) createPrivate 函數(shù),該函數(shù)會(huì)返回一個(gè)私有的 token 函數(shù),對(duì)外不可見(jiàn),對(duì)內(nèi)通過(guò)閉包函數(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í)例上并沒(méi)有 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)通過(guò)Symbol(‘privateStore')這個(gè)屬性來(lái)查看實(shí)例各個(gè)層次的私有部分。
奇葩的 es5 property getter 攔截方案
該方案純粹是閑得無(wú)聊玩了玩,主要是利用了 es5 提供的 getter,根據(jù) argument.callee.caller 去判斷調(diào)用場(chǎng)景,如果是外部的則拋異常或返回 undefined, 如果是內(nèi)部調(diào)用則返回真正的私有成員,實(shí)現(xiàn)起來(lái)比較復(fù)雜,且不支持 strict 模式,不推薦使用。 有興趣的同學(xué)可以看看實(shí)現(xiàn)。
總結(jié)
以上幾個(gè)方案對(duì)比下來(lái),我個(gè)人是傾向 Symbol+WeakMap 的整合方案,結(jié)合了兩者的優(yōu)點(diǎn),又彌補(bǔ)了 WeakMap 的不足和 Symbol 書寫的冗余。 當(dāng)然了,我相信隨著 JS 的發(fā)展,私有成員和保護(hù)成員也遲早會(huì)在語(yǔ)法層面上進(jìn)行支持,正如 es6 對(duì) class 關(guān)鍵字和 super 語(yǔ)法糖的支持一樣, 只是現(xiàn)階段需要開(kāi)發(fā)者使用一些技巧去填補(bǔ)語(yǔ)言特性上的空白。
Javascript私有成員的實(shí)現(xiàn)方式
總體來(lái)講這本書還是可以的,但看完這本書還留了幾個(gè)問(wèn)題一直困擾著我,如js中私有變量的實(shí)現(xiàn),prototype等,經(jīng)過(guò)自己一系列測(cè)試,現(xiàn)在終于弄明白了。
很多書上都是說(shuō),Javascript是不能真正實(shí)現(xiàn)Javascript私有成員的,因此在開(kāi)發(fā)的時(shí)候,統(tǒng)一約定 __ 兩個(gè)下劃線開(kāi)頭為私有變量。
后來(lái),發(fā)現(xiàn)Javascript中閉包的特性,從而徹底解決了Javascript私有成員的問(wèn)題。
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 根本訪問(wèn)不到,但是用對(duì)象方法能訪問(wèn)到,因?yàn)殚]包能從當(dāng)前的執(zhí)行環(huán)境中獲取信息。
接下來(lái)我們看看,共有成員是怎樣實(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
接下來(lái)在看看類靜態(tài)變量是怎樣實(shí)現(xiàn)的
function testFn(){
}
testFn.Name = "KenChen";
alert(testFn.Name);//KenChen
testFn.Name = "CC";
alert(testFn.Name);//CC
相關(guān)文章
JS實(shí)現(xiàn)頁(yè)面打?。ㄕw、局部)
本篇文章主要介紹了JS實(shí)現(xiàn)頁(yè)面打印(整體、局部),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08
ymPrompt的doHandler方法來(lái)實(shí)現(xiàn)獲取子窗口返回值的方法
今天在寫頁(yè)面時(shí)用到了ymPrompt的win方法來(lái)彈出一個(gè)窗口。由于要用到獲取子窗口返回來(lái)的值判斷是否刷新父窗口,在ymPrompt的組件Demo中一直沒(méi)有找到合適的方法實(shí)現(xiàn)2010-06-06
詳解js實(shí)時(shí)獲取并顯示當(dāng)前時(shí)間的方法
這篇文章主要介紹了js實(shí)時(shí)獲取并顯示當(dāng)前時(shí)間的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05
webpack4 配置 ssr 環(huán)境遇到“document is not defined”
這篇文章主要介紹了webpack4 配置 ssr 環(huán)境遇到“document is not defined”,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-10-10
JavaScript改變HTML元素的樣式改變CSS及元素屬性
改變CSS及元素屬性的方法有很多,在本文將為大家介紹下使用js是如何做到的,感興趣的朋友可以感受下2013-11-11
微信小程序?qū)崿F(xiàn)音樂(lè)播放頁(yè)面布局
淺談JavaScript宏任務(wù)和微任務(wù)執(zhí)行順序

