1. 程式人生 > >JS新語法:私有屬性

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辛勤工作的人們讓這成為現實。