JS新語法:私有屬性
譯者注
這個語法是框架開發者們非常期待的,這樣就可以有效區分使用者的名稱空間和框架內部欄位。
這個語法雖然已經到了stage-2但是提出時間還比較短,對應的Babel外掛還沒有釋出,所以還不能實際使用,可以先了解語法規則。
譯文
類私有欄位已經處於JavaScript規範流程的Stage 2,它還沒完成但是JS規範協會期望這個功能得到實現並歸入語言規範(雖然可能還有變數)。
該語法(目前)如下:
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(point) {
return this.#x === point.#x && this.#y === point.#y;
}
}
這個語法有兩個關鍵部分:
- 定義私有欄位
- 引用私有欄位
定義私有欄位
定義私有欄位跟定義公有欄位區別不大:
class Foo {
publicFieldName = 1;
#privateFieldName = 2;
}
欲引用一個私有欄位,必須先定義它,所以如果你不想在定義的時候給初始值,可以這樣:
class Foo {
#privateFieldName;
}
引用私有欄位
引用一個私有欄位跟引用其他欄位類似,只是有一個特殊的語法。
class Foo {
publicFieldName = 1;
#privateFieldName = 2;
add() {
return this.publicFieldName + this.#privateFieldName;
}
}
this.#還有個簡寫方法:
method() {
#privateFieldName;
}
其實跟這個效果相同:
method() {
this.#privateFieldName;
}
引用例項的私有欄位
引用私有欄位並不侷限於this,你還可以訪問同類其他例項裡的私有欄位:
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // >> 42
在這裡,foo是Foo的例項,所以可以在Foo類的定義裡面訪問foo.#privateValue。
私有方法(即將到來?)
私有欄位提案只是關注於新增類欄位,該提案沒有對類方法有任何改動,所以類私有方法即將出現在一個跟進提案當中,並且很可能長這樣:
class Foo {
constructor() {
this.#method();
}
#method() {
// ...
}
}
在這之前,你可以給私有欄位複製函式值:
class Foo {
constructor() {
this.#method();
}
#method = () => {
// ...
};
}
封裝性
如果你在使用一個類的例項,你不能引用該類的私有欄位。你只能在該類的定義裡引用它們。
class Foo {
#bar;
method() {
this.#bar; // Works
}
}
let foo = new Foo();
foo.#bar; // Invalid!
為了真正地體現私有,你不應該有辦法檢測一個私有欄位是否存在。
為了保證你無法檢測到一個私有欄位,我們需要私有欄位和公有自動可以重名。
class Foo {
bar = 1; // public bar
#bar = 2; // private bar
}
不然如果不允許同名,你就可以用如下方法檢測到私有欄位是否存在:
foo.bar = 1; // Error: `bar` is private! (boom... detected)
或者靜默的版本:
foo.bar = 1;
foo.bar; // `undefined` (boom... detected again)
這種封裝性對於子類也同樣試用。一個子類應當可以定義同名欄位而不用擔憂會影響父型別。
class Foo {
#fieldName = 1;
}
class Bar extends Foo {
fieldName = 2; // Works!
}
那為什麼是井號?
許多人會想:“為啥不按照其他語言的約定,用一個private關鍵字呢?”
這有一個這種語法的樣例:
class Foo {
private value;
equals(foo) {
return this.value === foo.value;
}
}
我們來分別看看這種語法的兩個部分。
為什麼定義的時候不用private關鍵字?
在很多語言裡都會使用private來定義私有欄位。
這種語言的語法如:
class EnterpriseFoo {
public bar;
private baz;
method() {
this.bar;
this.baz;
}
}
這些語言當中,公有和私有欄位的訪問方式是一樣的。所以這麼定義可以理解。
然而在JS當中,因為我們不能用this.field訪問私有屬性(我一會再講),我們就需要一種方法進行語法層面的關聯。通過在兩處都使用#,到底引用了什麼就很明顯了。
為什麼引用的時候需要用 #井號 ?
我們必須用this.#field而不是this.field有以下幾個原因:
- 為了封裝性(見上面封裝性章節),我們需要公有和私有欄位可以同時擁有相同的名字。所以訪問一個私有欄位不能是個普通的查詢。
- JS裡公有屬性可以通過this.field或者this[‘field’]訪問。然而私有屬性不支援第二個語法(因為它必須是靜態的),這會可能導致混淆。
- 你需要付出不少效能帶價來做型別檢查:
來看看一個程式碼例子:
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(other) {
return this.#x === other.#x && this.#y === other.#y;
}
}
注意我們是如何引用other.#x以及other.#y。使用私有欄位,我們就在假定該例項是我們Point類的一個例項。
因為我們使用了 # 語法,所以也就告知了JS編譯器我們是在當前類裡的私有屬性。
如果我們沒用 # 會怎樣?
equals(otherPoint) {
return this.x === otherPoint.x && this.y === otherPoint.y;
}
我們有個問題:我們如何知道otherPoint是什麼?
JavaScript沒有一個靜態型別系統,所以otherPoint什麼都可能是。
這就產生問題了:
- 我們的函式根據傳入引數的不同會有不同的表現:有時會訪問一個私有屬性,又有時訪問共有屬性。
- 我們每次都需要檢查otherPoint的型別:
if (
otherPoint instanceof Point &&
isNotSubClass(otherPoint, Point)
) {
return getPrivate(otherPoint, 'foo');
} else {
return otherPoint.foo;
}
更糟糕的是,我們每一個屬性訪問都需要檢查一下是不是在引用私有屬性。
屬性訪問已經挺慢的,所以我們真的不想再給它增加負擔了。
上面太長;不看:
我們需要用 # 來使用私有屬性是因為如果不用,會產生不可預料的表現並且造成巨大的效能影響。
結語
私有屬性是對語言的一個帥氣加強。感謝所有為TC39辛勤工作的人們讓這成為現實。