前幾年我曾經寫過「7天搞懂JS進階議題」系列文章,你可以在我的網站或是CoderBridge閱讀。其中在番外篇提到過「隱私成員」,在當時因為JavaScript並沒有隱私屬性的設計,所以想實現,當時使用了閉包和屬性描述器來處理。
時過境遷,在我寫完發表沒多久,ES11(2020)也正式推出了,其中就有關於私有屬性和私有方法的設計。根據Can I Use使用支援程度已經超過九成,也就是在今天主要瀏覽器除了一些舊版本和特別的瀏覽器外應該多數也都支援了。
class Person {
name = "";
birthday = new Date();
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.birthday = birthday;
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // 變更生日
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
如果設計了一個類別Person
有名字(name
)、生日(birthday
)、地址(addr
)等屬性,從嘗試性來說生日在出生以後就不能夠變了,但是上面程式碼片段當我們建立一個實例物件bob
後,依然可以變更生日。
如果學著Python的做法,程式碼可能會變成這麼設計:
class Person {
name = "";
_birthday = new Date();
get birthday() { return this._birthday }; // 調整處
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this._birthday = birthday; // 調整處
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // 沒有作用,無法變更生日
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
但這會與Python有著同樣的問題,並不是真的無法變更生日:
bob._birthday = new Date(); // 調整 `_birthday`
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
而且JavaScript並沒有大量的習慣以底線開頭的就是私有屬性,很多時候只是團隊內的默契而已。
如果你有在寫TypeScript,會知道有一個private
的關鍵字。那麼上面程式就會變成:
class Person {
name: string = "";
private birthday: Date = new Date();
addr: string = ""
get age(): number {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name: string, birthday: Date, addr: string = "") {
this.name = name;
this.birthday = birthday;
this.addr = addr;
}
hello(): void {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // TypeScript標記錯誤
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
雖然這樣寫在TypeScript轉換成JavaScript時就會提示問題,但是這仍然不是無法修改:
// @ts-ignore 忽略下行錯誤問題
bob.birthday = new Date();
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年0歲
一般來說屬性通常是字母或底線開頭的字串,不能夠是數字或其他特殊字元開頭:
class EN {
1abc = null; // 拋出錯誤
}
不過在ES11後,允許屬性或方法的命名可以以#
開頭,並且帶有特殊意義。沒錯!就是私有屬性或私有方法。因此我們可以在將程式改寫成:
class Person {
name = "";
#birthday = new Date(); // 私有屬性
get birthday() { return this.#birthday }; // 調整處
addr = ""
get age() {
return new Date().getYear() - this.birthday.getYear();
}
constructor(name, birthday, addr = "") {
this.name = name;
this.#birthday = birthday; // 調整處
this.addr = addr;
}
hello() {
console.log(`Hello, ${this.name}.`);
}
}
var bob = new Person(/* name = */ "Bob",
/* birthday = */ new Date(2004/* year */,
1 /* mouth */,
1 /* day */),
/* addr = */ "臺灣")
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
bob.hello();
bob.birthday = new Date(); // 沒有作用,無法變更生日
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
並且嘗試在外部修改#birthday
也會報錯出問題:
bob.#birthday = new Date(); // 不能這麼做,會出錯
console.log(`${bob.name}今年${bob.age}歲`);
而且注意的是,這與使用字串嘗試存取是不同的,儘管其他屬性可以這麼做:
bob["name"] === bob.name; // 這兩者寫法可以取得同樣物件
bob["#birthday"] = new Date(); // 但這不意味著將私有屬性賦值
console.log(bob["#birthday"]); // 雖然 `bob["#birthday"]` 確實被建立且賦值了
console.log(bob.birthday); // 但是私有屬性並沒有被改變
console.log(`${bob.name}今年${bob.age}歲`); // Bob今年18歲
寫到這,我突然想到其實是有禁止添加新的屬性的方式。
不過我大多都是設計簡單資料類別,或是繼承框架類別進行擴展。
前端上這個需求好像不常見,或許以Node.js作為後端服務器會比較容易用到...之後再看看有沒有機會提到吧~
這個特性並不是只有屬性有,同樣可以應用在方法成為私有方法:
class C {
#innerFn() {
console.log("called #innerFn()");
}
outerFn() {
console.log("start outerFn()");
this.#innerFn();
console.log("end outerFn()");
}
}
var inst = new C();
inst.outerFn();
/****** Output ***********
start outerFn()
called #innerFn()
end outerFn()
************************/
inst.#innerFn(); // 不能這麼做,拋出錯誤
注意,並不能夠在建構式直接建立私有屬性。私有屬性和私有方法都必須現在class
關鍵字模板裡宣告。
class C {
constructor() {
this.#a = "A";
}
}
所以這樣也是行不通的:
var inst = {
#a: "A", // 不能這麼做
}
以上差不多就是該知道的ES11的私有屬性和私有方法的內容了。
本文同時發表於我的隨筆