私有屬性
私有屬性是常規類屬性(包括類欄位、類方法等)的對應項,這些屬性是公開的。私有屬性透過使用雜湊 # 字首建立,並且不能在類外部合法地引用。這些類屬性的隱私封裝由 JavaScript 本身強制執行。訪問私有屬性的唯一方法是透過點表示法,並且只能在定義私有屬性的類中執行此操作。
在該語法存在之前,私有屬性不是語言的原生特性。在原型繼承中,可以使用WeakMap物件或閉包模擬其行為,但它們在人體工程學方面無法與 # 語法相媲美。
語法
class ClassWithPrivate {
#privateField;
#privateFieldWithInitializer = 42;
#privateMethod() {
// …
}
static #privateStaticField;
static #privateStaticFieldWithInitializer = 42;
static #privateStaticMethod() {
// …
}
}
還有一些額外的語法限制
- 在類中宣告的所有私有識別符號必須是唯一的。名稱空間在靜態屬性和例項屬性之間共享。唯一的例外是當兩個宣告定義 getter-setter 對時。
- 私有識別符號不能是
#constructor。
描述
大多數類屬性都有其私有對應項
- 私有欄位
- 私有方法
- 私有靜態欄位
- 私有靜態方法
- 私有 getter
- 私有 setter
- 私有靜態 getter
- 私有靜態 setter
這些特性統稱為私有屬性。但是,建構函式在 JavaScript 中不能是私有的。為了防止在類外部構造類,您必須使用私有標誌。
私有屬性用# 名稱(發音為“雜湊名稱”)宣告,這些名稱是帶有 # 字首的識別符號。雜湊字首是屬性名稱的固有部分——您可以將其與舊的下劃線字首約定 _privateField 聯絡起來——但它不是普通的字串屬性,因此您不能使用方括號表示法動態訪問它。
從類外部引用 # 名稱是語法錯誤。引用在類主體中未宣告的私有屬性或嘗試使用delete刪除已宣告的屬性也是語法錯誤。
class ClassWithPrivateField {
#privateField;
constructor() {
delete this.#privateField; // Syntax error
this.#undeclaredField = 42; // Syntax error
}
}
const instance = new ClassWithPrivateField();
instance.#privateField; // Syntax error
JavaScript 作為一種動態語言,能夠執行此編譯時檢查,因為特殊的雜湊識別符號語法使其在語法級別上與普通屬性不同。
注意:在 Chrome 控制檯中執行的程式碼可以在類外部訪問私有屬性。這是 JavaScript 語法限制的 DevTools 獨有放鬆。
如果從沒有該屬性的物件訪問私有屬性,則會丟擲TypeError,而不是像普通屬性那樣返回 undefined。
class C {
#x;
static getX(obj) {
return obj.#x;
}
}
console.log(C.getX(new C())); // undefined
console.log(C.getX({})); // TypeError: Cannot read private member #x from an object whose class did not declare it
此示例還說明您也可以在靜態函式中以及在類的外部定義的例項上訪問私有屬性。
您可以使用in運算子檢查外部定義的物件是否擁有私有屬性。如果私有欄位或方法存在,則返回 true,否則返回 false。
class C {
#x;
constructor(x) {
this.#x = x;
}
static getX(obj) {
if (#x in obj) return obj.#x;
return "obj must be an instance of C";
}
}
console.log(C.getX(new C("foo"))); // "foo"
console.log(C.getX(new C(0.196))); // 0.196
console.log(C.getX(new C(new Date()))); // the current date and time
console.log(C.getX({})); // "obj must be an instance of C"
請注意私有名稱始終預先宣告且不可刪除的推論:如果您發現物件擁有當前類的某個私有屬性(來自 try...catch 或 in 檢查),則它必須擁有所有其他私有屬性。擁有類私有屬性的物件通常表示它是由該類構造的(儘管並非總是如此)。
私有屬性不是原型繼承模型的一部分,因為它們只能在當前類的主體中訪問,並且不會被子類繼承。不同類中名稱相同的私有屬性完全不同,並且不會相互互動。將它們視為附加到每個例項的外部元資料,由類管理。因此,structuredClone()不會克隆私有屬性,並且Object.freeze()和Object.seal()對私有屬性沒有影響。
有關如何以及何時初始化私有欄位的更多資訊,請參閱公共類欄位。
示例
私有欄位
私有欄位包括私有例項欄位和私有靜態欄位。私有欄位只能從類宣告內部訪問。
私有例項欄位
與它們的公共對應項一樣,私有例項欄位
- 在基類中執行建構函式之前新增,或者在子類中呼叫
super()之後立即新增,並且 - 僅在類的例項上可用。
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
}
}
class Subclass extends ClassWithPrivateField {
#subPrivateField;
constructor() {
super();
this.#subPrivateField = 23;
}
}
new Subclass(); // In some dev tools, it shows Subclass {#privateField: 42, #subPrivateField: 23}
注意:來自 ClassWithPrivateField 基類的 #privateField 對 ClassWithPrivateField 是私有的,並且無法從派生的 Subclass 訪問。
返回覆蓋物件
類的建構函式可以返回一個不同的物件,該物件將用作派生類建構函式的新 this。然後,派生類可以在該返回的物件上定義私有欄位——這意味著可以將私有欄位“標記”到不相關的物件上。
class Stamper extends class {
// A base class whose constructor returns the object it's given
constructor(obj) {
return obj;
}
} {
// This declaration will "stamp" the private field onto the object
// returned by the base class constructor
#stamp = 42;
static getStamp(obj) {
return obj.#stamp;
}
}
const obj = {};
new Stamper(obj);
// `Stamper` calls `Base`, which returns `obj`, so `obj` is
// now the `this` value. `Stamper` then defines `#stamp` on `obj`
console.log(obj); // In some dev tools, it shows {#stamp: 42}
console.log(Stamper.getStamp(obj)); // 42
console.log(obj instanceof Stamper); // false
// You cannot stamp private properties twice
new Stamper(obj); // Error: Initializing an object twice is an error with private fields
警告:這可能是一件非常令人困惑的事情。通常建議您避免從建構函式中返回任何內容——尤其是與 this 不相關的內容。
私有靜態欄位
與它們的公共對應項一樣,私有靜態欄位
- 在類評估時新增到類建構函式中,並且
- 僅在類本身可用。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return ClassWithPrivateStaticField.#privateStaticField;
}
}
console.log(ClassWithPrivateStaticField.publicStaticMethod()); // 42
私有靜態欄位有一個限制:只有定義私有靜態欄位的類才能訪問該欄位。在使用this時,這可能會導致意外行為。在以下示例中,當我們嘗試呼叫 Subclass.publicStaticMethod() 時,this 指的是 Subclass 類(而不是 ClassWithPrivateStaticField 類),因此會導致 TypeError。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {}
Subclass.publicStaticMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
如果您使用 super 呼叫該方法,則情況相同,因為super 方法不是以超類作為 this 呼叫的。
class ClassWithPrivateStaticField {
static #privateStaticField = 42;
static publicStaticMethod() {
// When invoked through super, `this` still refers to Subclass
return this.#privateStaticField;
}
}
class Subclass extends ClassWithPrivateStaticField {
static callSuperMethod() {
return super.publicStaticMethod();
}
}
Subclass.callSuperMethod(); // TypeError: Cannot read private member #privateStaticField from an object whose class did not declare it
建議您始終透過類名訪問私有靜態欄位,而不是透過 this 訪問,這樣繼承就不會破壞該方法。
私有方法
私有方法包括私有例項方法和私有靜態方法。私有方法只能從類宣告內部訪問。
私有例項方法
與它們的公共對應項不同,私有例項方法
- 在安裝例項欄位之前立即安裝,並且
- 僅在類的例項上可用,而不是在其
.prototype屬性上可用。
class ClassWithPrivateMethod {
#privateMethod() {
return 42;
}
publicMethod() {
return this.#privateMethod();
}
}
const instance = new ClassWithPrivateMethod();
console.log(instance.publicMethod()); // 42
私有例項方法可以是生成器、非同步或非同步生成器函式。私有 getter 和 setter 也是可能的,並且遵循與其公共 getter 和 setter 對應部分相同的語法要求。
class ClassWithPrivateAccessor {
#message;
get #decoratedMessage() {
return `🎬${this.#message}🛑`;
}
set #decoratedMessage(msg) {
this.#message = msg;
}
constructor() {
this.#decoratedMessage = "hello world";
console.log(this.#decoratedMessage);
}
}
new ClassWithPrivateAccessor(); // 🎬hello world🛑
與公共方法不同,私有方法無法在類的 .prototype 屬性上訪問。
class C {
#method() {}
static getMethod(x) {
return x.#method;
}
}
console.log(C.getMethod(new C())); // [Function: #method]
console.log(C.getMethod(C.prototype)); // TypeError: Receiver must be an instance of class C
私有靜態方法
與它們的公共對應部分一樣,私有靜態方法
- 在類評估時新增到類建構函式中,並且
- 僅在類本身可用。
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return ClassWithPrivateStaticMethod.#privateStaticMethod();
}
}
console.log(ClassWithPrivateStaticMethod.publicStaticMethod()); // 42
私有靜態方法可以是生成器、非同步和非同步生成器函式。
之前提到的私有靜態欄位的相同限制也適用於私有靜態方法,並且類似地,在使用 this 時可能導致意外行為。在以下示例中,當我們嘗試呼叫 Subclass.publicStaticMethod() 時,this 指的是 Subclass 類(而不是 ClassWithPrivateStaticMethod 類),因此會導致 TypeError。
class ClassWithPrivateStaticMethod {
static #privateStaticMethod() {
return 42;
}
static publicStaticMethod() {
return this.#privateStaticMethod();
}
}
class Subclass extends ClassWithPrivateStaticMethod {}
console.log(Subclass.publicStaticMethod()); // TypeError: Cannot read private member #privateStaticMethod from an object whose class did not declare it
模擬私有建構函式
許多其他語言都包含將建構函式標記為私有的功能,這可以防止類在類本身之外被例項化——你只能使用建立例項的靜態工廠方法,或者根本無法建立例項。JavaScript 沒有原生方法來做到這一點,但可以透過使用私有靜態標誌來實現。
class PrivateConstructor {
static #isInternalConstructing = false;
constructor() {
if (!PrivateConstructor.#isInternalConstructing) {
throw new TypeError("PrivateConstructor is not constructable");
}
PrivateConstructor.#isInternalConstructing = false;
// More initialization logic
}
static create() {
PrivateConstructor.#isInternalConstructing = true;
const instance = new PrivateConstructor();
return instance;
}
}
new PrivateConstructor(); // TypeError: PrivateConstructor is not constructable
PrivateConstructor.create(); // PrivateConstructor {}
規範
| 規範 |
|---|
| ECMAScript 語言規範 # prod-PrivateIdentifier |
| ECMAScript 語言規範 # prod-00OK517S |
瀏覽器相容性
javascript.classes.private_class_fields
BCD 表格僅在瀏覽器中載入
javascript.classes.private_class_fields_in
BCD 表格僅在瀏覽器中載入
javascript.classes.private_class_methods
BCD 表格僅在瀏覽器中載入
另請參閱
- 使用類 指南
- 類
- 公共類欄位
class- TC39 類欄位提案中的私有語法常見問題解答
- 所有 JS 類元素的語義,作者:郭舒羽 (2018)
- v8.dev 上的公共和私有類欄位 (2018)