今天在看《你不知道的js》這本書時,無意看到Object還有個方法叫做 getOwnPropertySymbols() ,用來獲取對象的Symbol屬性。記得之前看過一些文章說,可以使用Symbol來實現(xiàn)私有屬性,如果能直接使用 getOwnPropertySymbols() 方法獲取Symbol屬性,那還是私有屬性么?
今天復(fù)習(xí)整理一下關(guān)于js中創(chuàng)建私有屬性的一些問題。
由于js并不是Java那種類式面向?qū)ο?,因此即使es6添加了class支持,js根上還是基于原型的面向?qū)ο螅恢С质裁此接?,公有屬性的?/p>
要想實現(xiàn)私有屬性,基于現(xiàn)有的js,途徑只有一個: 閉包。
至于什么是閉包,網(wǎng)上一大把,有興趣也可以看看我之前整理的文章。
使用原始的閉包
const User = (function () {
return class User{
constructor(name) {
this.getName = function () {
return name;
}
this.setName = function (name) {
this.getName = function () {
return name;
}
}
}
};
})();
這種方式的確可以實現(xiàn)私有的屬性,而且如果有子類繼承,也可以如下寫法:
const Boy = (function () {
return class Boy extends User{
constructor(name){
super(name);
this.getGender = function () {
return 'boy';
}
}
}
})();
但問題也有:
- 代碼組織混亂。由于對構(gòu)造器函數(shù)形成一個閉包,因此所有的setter,getter函數(shù)都寫在了構(gòu)造器內(nèi)??梢詮纳厦娴腢ser的寫法中看出,setter函數(shù)中,還要再定義一遍getter,這中混亂,不是一般能忍受的。如果業(yè)務(wù)邏輯中還有其他的更多操作,那么混亂程度一下子就上來了。
- 閉包的內(nèi)存開銷不容小覷。
針對上面的問題,我們可不可以將name單獨拿出來放到單獨一個地方存儲,優(yōu)化一下代碼的組織呢?比如下面這個:
const User = (function () {
let privateName = null;
return class User{
constructor(name) {
privateName = name;
}
getName(){
return privateName;
}
setName(name) {
privateName = name;
}
};
})();
上面這個例子,貌似是可以的,這樣代碼也清晰了,也能實現(xiàn)私有屬性。
但是,仔細(xì)分析一下,真的可以么?
如此的話,是不是所有的實例對象都共享一個 privateName 屬性?后面的實例會覆蓋前面實例的值??聪旅妫?/p>
const u1 = new User('coolcao');
const u2 = new User('lili');
console.log(u2.getName());
console.log(u1.getName());
// lili
// lili
針對上面的問題,我們使用一個 privateName 保存私有屬性,會被覆蓋,那么我們?nèi)绻褂靡粋€數(shù)組,保存多個,然后針對每個實例,生成一個唯一的存取標(biāo)識呢?
基于散列實現(xiàn)
const User = (function () {
const privateData = {};
let i = 0;
return class User {
constructor(name){
this['_id'] = i ++;
privateData[this['_id']] = {name: name};
}
getName() {
return privateData[this['_id']].name;
}
setName(name) {
privateData[this['_id']].name = name;
}
};
})();
我們使用 privateData 這個對象來保存所有實例的私有屬性,針對每個實例,使用一個id進(jìn)行標(biāo)識,每實例化一個實例,該id會自動加1。然后將該id作為鍵值,將私有屬性存入 privateData 對象。
嗯,這樣看上去比直接在構(gòu)造器中使用閉包要清晰多了,而且實例與實例之間也不沖突。
在es6之前,這可能是最合適的方案。雖然也會存在問題:
每個實例對象都會引用 privateData ,因此,還是由于閉包的問題,如果實例太多的話,內(nèi)存是個問題。
WeakMap實現(xiàn)
幸好 es6 來了,帶來了一個叫做 WeakMap 的東西,具體這東西是啥呢?可以看看阮老師的教程
簡單說來,WeakMap鍵名所引用的對象都是弱引用,即垃圾回收機制不將該引用考慮在內(nèi)。因此,只要所引用的對象的其他引用都被清除,垃圾回收機制就會釋放該對象所占用的內(nèi)存。也就是說,一旦不再需要,WeakMap 里面的鍵名對象和所對應(yīng)的鍵值對會自動消失,不用手動刪除引用。
好了,那么上面的這個強引用關(guān)系,我們可以使用 WeakMap 弱引用來代替:
const User = (function () {
const privateData = new WeakMap();
class User {
constructor(name) {
privateData.set(this,{name:name});
}
getName() {
return privateData.get(this).name;
}
setName(name) {
privateData.get(this).name = name;
}
}
})();
如此的代碼,干凈清爽了許多,而且由于WeakMap是弱引用,如果沒有其他引用和該鍵引用同一個對象,這個對象將會被當(dāng)作垃圾回收掉。解決了內(nèi)存泄露的問題。
好了,js模擬閉包,就這幾個方式了,從這幾個例子來看,都使用了自執(zhí)行函數(shù)(IIFE),因此都會形成閉包。這也是我最開始說的,要想在js實現(xiàn)私有屬性,只能使用閉包。
Symbol的問題
話說回來我當(dāng)初的疑問,ES6的Symbol實現(xiàn)的私有屬性有啥問題呢?
看下面例子:
const User = (function () {
const NAME = Symbol('User#Name');
return class User{
constructor(name){
this[NAME] = name;
}
getName(){
return this[NAME];
}
setName(name) {
this[NAME] = name;
}
}
})();
NAME是個Symbol,從外部并不能拿到NAME確切的值,好像是有點私有屬性的意思。但是 有一個 Object.getOwnPropertySymbols() 方法可以拿到對象所有的Symbol屬性,雖然我不知道具體存了個啥,但是能拿到這個標(biāo)識,就可以修改屬性值了:
const smbs = Object.getOwnPropertySymbols(user);
for(let s of smbs) {
user[s] = 'good';
}
console.log(user.getName());
// good
因此,嚴(yán)格意義上說,Symbol其實并不能實現(xiàn)私有屬性。
不過倒是可以將上面第二種基于散列的方式改為Symbol的方式:
const User = (function () {
const privateData = {};
return class User {
constructor(name){
this._name = Symbol('name');
privateData[this._name] = {name: name};
}
getName() {
return privateData[this._name].name;
}
setName(name) {
privateData[this._name].name = name;
}
};
})();
這樣對外暴露的只是 _name 的這個符號,外部還是無法直接訪問每個實例的 privateData 中的值。但這個其實和第二種是一樣的,閉包引起的問題還是無法解決。
展望未來
js的私有屬性,目前處于 stage2 階段,目前還未最終確定,不過我們可以先看一下模樣:
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(point) {
return this.#x === point.#x && this.#y === point.#y;
}
}
語法如上,使用 # 定義私有屬性,在類的內(nèi)部可以直接 使用 this.#x 的形式引用。
目前使用#進(jìn)行定義和訪問私有屬性,未來會不會使用 public,private等關(guān)鍵字不得而知。
目前使用#,可能的原因是,js沒有靜態(tài)類型系統(tǒng)。