?JavaScript私有屬性的多種實現(xiàn)方式總匯
點擊上方?前端Q,關(guān)注公眾號
回復(fù)加群,加入前端Q技術(shù)交流群
來源 | http://www.fly63.com
JavaScript被很多人認(rèn)為并不是一種面向?qū)ο笳Z言,原因有很多種,比如JavaScript沒有類,不能提供傳統(tǒng)的類式繼承;再比如JavaScript不能實現(xiàn)信息的隱藏,不能實現(xiàn)私有成員。
本文并不是為了打破以上誤解(實際上筆者自己也有困惑),只是簡單介紹幾種JavaScript實現(xiàn)私有屬性的方式,以及各自的優(yōu)劣。
1、基于編碼規(guī)范約定實現(xiàn)方式
很多編碼規(guī)范把以下劃線_開頭的變量約定為私有成員,便于同團隊開發(fā)人員的協(xié)同工作。實現(xiàn)方式如下:
function Person(name){this._name = name;}var person = new Person('Joe');
這種方式只是一種規(guī)范約定,很容易被打破。而且也并沒有實現(xiàn)私有屬性,上述代碼中的實例person可以直接訪問到_name屬性:
alert(person._name); //'Joe'
2、?基于閉包的實現(xiàn)方式
另外一種比較普遍的方式是利用JavaScript的閉包特性。構(gòu)造函數(shù)內(nèi)定義局部變量和特權(quán)函數(shù),其實例只能通過特權(quán)函數(shù)訪問此變量,如下:
function Person(name){var _name = name;this.getName = function(){return _name;}}var person = new Person('Joe');
這種方式的優(yōu)點是實現(xiàn)了私有屬性的隱藏,Person 的實例并不能直接訪問_name屬性,只能通過特權(quán)函數(shù)getName獲?。?/span>
alert(person._name); // undefinedalert(person.getName()); //'Joe'
使用閉包和特權(quán)函數(shù)實現(xiàn)私有屬性的定義和訪問是很多開發(fā)者采用的方式。但是這種方式存在一些缺陷:
私有變量和特權(quán)函數(shù)只能在構(gòu)造函數(shù)中創(chuàng)建。通常來講,構(gòu)造函數(shù)的功能只負責(zé)創(chuàng)建新對象,方法應(yīng)該共享于prototype上。特權(quán)函數(shù)本質(zhì)上是存在于每個實例中的,而不是prototype上,增加了資源占用。
3、?基于強引用散列表的實現(xiàn)方式
JavaScript不支持Map數(shù)據(jù)結(jié)構(gòu),所謂強引用散列表方式其實是Map模式的一種變體。簡單來講,就是給每個實例新增一個唯一的標(biāo)識符,以此標(biāo)識符為key,對應(yīng)的value便是這個實例的私有屬性,這對key-value保存在一個Object內(nèi)。實現(xiàn)方式如下:
var Person = (function() {var privateData = {},privateId = 0;function Person(name) {Object.defineProperty(this, "_id", { value: privateId++ });privateData[this._id] = {name: name};}Person.prototype.getName = function() {return privateData[this._id].name;};return Person;}());
上述代碼的有以下幾個特征:
使用自執(zhí)行函數(shù)創(chuàng)建Person類,變量privateData和privateId被所有實例共享;
privateData用來儲存每個實例的私有屬性name的key-value,privateId用來分配每個實例的唯一標(biāo)識符_id;
方法getName存在于prototype上,被所有實例共享。
這種方式在目前ES5環(huán)境下,基本是最佳方案了。但是仍然有一個致命的缺陷:散列表privateData對每個實例都是強引用,導(dǎo)致實例不能被垃圾回收處理。如果存在大量實例必然會導(dǎo)致memory leak。
造成以上問題的本質(zhì)是JavaScript的閉包引用,以及只能使用字符串類型最為散列表的key值。針對這兩個問題,ES6新增的WeakMap可以良好的解決。
4、基于WeakMap的實現(xiàn)方式
WeakMap有以下特點:
1)、支持使用對象類型作為key值;
2)、弱引用。
根據(jù)WeakMap的特點,便不必為每個實例都創(chuàng)建一個唯一標(biāo)識符,因為實例本身便可以作為WeakMap的key。改進后的代碼如下:
var Person = (function() {var privateData = new WeakMap();function Person(name) {privateData.set(this, { name: name });}Person.prototype.getName = function() {return privateData.get(this).name;};return Person;}());
改進的代碼不僅僅干凈了很多,而且WeakMap是一種弱引用散列表, 這意味著,如果沒有其他引用和該鍵引用同一個對象,這個對象將會被當(dāng)作垃圾回收掉。解決了內(nèi)存泄露的問題。
不幸的是,目前瀏覽器對WeakMap的支持率并不理想,投入生產(chǎn)環(huán)境仍然需要等待。
5、基于Proxy約束
Proxy 可以定義目標(biāo)對象的 get、set、Object.keys 的邏輯,可以在這一層做一下判斷,如果是下劃線 _ 開頭就不讓訪問,否則就可以訪問。
比如還是這個 class:
class Dong {constructor() {this._name = 'dong';this._age = 20;this.friend = 'guang';}hello() {return 'I\'m ' + this._name + ', ' + this._age + ' years old';}}const dong = new Dong();
我們不直接調(diào)用它的對象的屬性方法了,而是先用一層 Proxy 來約束下 get、set、getKeys 的行為:
const dong = new Dong();const handler = {get(target, prop) {if (prop.startsWith('_')) {return;}return target[prop];},set(target, prop, value) {if (prop.startsWith('_')) {return;}target[prop] = value;},ownKeys(target, prop) {return Object.keys(target).filter(key => !key.startsWith('_'))},}const proxy = new Proxy(dong, handler)
我們通過 new Proxy 來給 dong 定義了 get、set、ownKeys 的 handler:
get:?如果以下劃線 _ 開頭就返回空,否則返回目標(biāo)對象的屬性值 target[prop]。
set:?如果以下劃線 _ 開頭就直接返回,否則設(shè)置目標(biāo)對象的屬性值。
ownKeys:?訪問 keys 時,過濾掉目標(biāo)對象中下劃線開頭的屬性再返回。
這樣就實現(xiàn)了下劃線開頭的屬性的私有化:
我們測試下:
const proxy = new Proxy(dong, handler)for (const key of Object.keys(proxy)) {console.log(key, proxy[key])}
確實,這里只打印了共有屬性的方法,而下劃線開頭的那兩個屬性沒有打印。我們基于 _prop 這種命名規(guī)范實現(xiàn)了真正的私有屬性!
6、Symbol用于創(chuàng)建唯一的值
Symbol 是 es2015 添加的一個 api,用于創(chuàng)建唯一的值?;谶@個唯一的特性,我們就可以實現(xiàn)私有屬性。
比如這樣:
const nameSymbol = Symbol('name');const ageSymbol = Symbol('age');class Dong {constructor() {this[nameSymbol] = 'dong';this[ageSymbol] = 20;}hello() {return 'I\'m ' + this[nameSymbol] + ', ' + this[ageSymbol] + ' years old';}}const dong = new Dong();
我們不再用 name 和 age 作為私有屬性名了,而是用 Symbol 生成唯一的值來作為名字。
7、es新草案 #prop
現(xiàn)在有一個私有屬性的 es 草案,可以通過 # 的方式來標(biāo)識私有屬性和方法。
比如這樣:
class Dong {constructor() {this.#name = 'dong';this.#age = 20;this.friend = 'guang';}hello() {return 'I\'m ' + this.#name + this.#age + 'years old';}}
這里的 name 和 age 都是私有的,而 friend 是共有的。
這種新語法 JS 引擎沒那么快支持,但是可以通過 babel 或者 ts 編譯器來編譯成低版本語法的方式來提前用。
比如 babel 有 @babel/proposal-private-property-in-object 的插件,它可以實現(xiàn)這種語法的編譯。

往期推薦



最后
歡迎加我微信,拉你進技術(shù)群,長期交流學(xué)習(xí)...
歡迎關(guān)注「前端Q」,認(rèn)真學(xué)前端,做個專業(yè)的技術(shù)人...


