ES 草案:class私有屬性

ES 草案:class私有屬性

class屬性是關於直接在類體內創建屬性和類似構造,這篇播客文章是關於它們的系列文章的一部分:

  1. 公有class屬性
  2. 私有class屬性

在這篇文章中,我們看看*私有屬性*,在類和實例中的一種新的私有插槽。這個功能是ES草案[JavaScript類屬性聲明]的一部分。

1.概覽

Private fields是一種與屬性不同的新的數據插槽。它們只能在它們聲明的class body內直接訪問。

1.1 靜態私有屬性

class MyClass {
 // 聲明並初始化靜態私有屬性
 static #privateStaticField = 1;
 static getPrivateStaticField() {
 return MyClass.#privateStaticField; // (A)
 }
}
assert.throws(
 () => eval('MyClass.#privateStaticField'),
 {
 name: 'SyntaxError',
 message: 'Undefined private field undefined:' +
 ' must be declared in an enclosing class',
 }
);
assert.equal(MyClass.getPrivateStaticField(), 1);

小竅門:永遠不要用`this`來訪問一個靜態私有屬性,直接使用類名訪問即可(就像A行那樣)。為什麼會在本文後面解釋。

1.2 實例私有屬性

使用帶有初始值的私有屬性(等號後面跟值)。

class MyClass {
 // 聲明並初始化
 #privateInstanceField = 2;
 getPrivateInstanceField() {
 return this.#privateInstanceField;
 }
}
assert.throws(
 () => eval('new MyClass().#privateInstanceField'),
 {
 name: 'SyntaxError',
 message: 'Undefined private field undefined:' +
 ' must be declared in an enclosing class',
 }
);
assert.equal(new MyClass().getPrivateInstanceField(), 2);

使用未初始化的實例私有屬性:

class DataStore {
 #data; // 必須聲明
 constructor(data) {
 this.#data = data;
 }
 getData() {
 return this.#data;
 }
}
assert.deepEqual(
 Reflect.ownKeys(new DataStore()),
 []);

2. 從下劃線到實例私有屬性

在JavaScript中保持數據私有的公共約定是使用帶有下劃線的屬性名。在這個部分,我們從使用這個約定的代碼開始,然後更改它,到使用實例私有屬性。

2.1 從下劃線開始

class Countdown {
 constructor(counter, action) {
 this._counter = counter; // private
 this._action = action; // private
 }
 dec() {
 if (this._counter < 1) return;
 this._counter--;
 if (this._counter === 0) {
 this._action();
 }
 }
}
// 變量並非真正的私有
assert.deepEqual(
 Reflect.ownKeys(new Countdown(5, () => {})),
 ['_counter', '_action']);

這個約定並沒有給我們任何保護;它僅僅建議使用這個類的用戶:不要使用這些屬性,它們是私有的。

2.2 切換到實例私有屬性

我們可以用實例私有屬性代替下劃線:

1. 我們用哈希符`#`替換每一個下劃線`_`。

2. 我們在類的頂部聲明所有的私有屬性。

class Countdown {
 #counter;
 #action;
 constructor(counter, action) {
 this.#counter = counter;
 this.#action = action;
 }
 dec() {
 if (this.#counter < 1) return;
 this.#counter--;
 if (this.#counter === 0) {
 this.#action();
 }
 }
}
// The data is now private:
assert.deepEqual(
 Reflect.ownKeys(new Countdown(5, () => {})),
 []);

3. 類主體內的所有代碼都可以訪問私有屬性

例如,實例方法可以訪問靜態私有屬性:

class MyClass {
 static #privateStaticField = 1;
 getPrivateFieldOfClass(theClass) {
 return theClass.#privateStaticField;
 }
}
assert.equal(
 new MyClass().getPrivateFieldOfClass(MyClass), 1);

一個靜態方法可以訪問實例私有屬性:

class MyClass {
 #privateInstanceField = 2;
 static getPrivateFieldOfInstance(theInstance) {
 return theInstance.#privateInstanceField;
 }
}
assert.equal(
 MyClass.getPrivateFieldOfInstance(new MyClass()), 2);

4.(高級)

剩餘部分是關於類的私有屬性的高級用法

5. 深入理解私有屬性

在規範中,私有屬性通過附加到對象上的數據結構進行管理。私有屬性的大致處理過程如下:

{
 // Private names
 const _counter = {
 __Description__: 'counter',
 __Kind__: 'field',
 };
 const _action = {
 __Description__: 'action',
 __Kind__: 'field',
 };
 class Object {
 // Maps private names to values
 __PrivateFieldValues__ = new Map();
 }
 class Countdown {
 constructor(counter, action) {
 this.__PrivateFieldValues__.set(_counter, counter);
 this.__PrivateFieldValues__.set(_action, action);
 }
 dec() {
 if (this.__PrivateFieldValues__.get(_counter) < 1) return;
 this.__PrivateFieldValues__.set(
 _counter, this.__PrivateFieldValues__.get(_counter) - 1);
 
 if (this.__PrivateFieldValues__.get(_counter) === 0) {
 this.__PrivateFieldValues__.get(_action)();
 }
 }
 }
}

這裡有兩點很重要:

  • 私有名稱是唯一的key。它們只能在class內訪問。
  • 私有屬性的值是一個私有名稱(key)=>值(value)的字典。每個擁有私有屬性的實例都有這樣一個字典。只能通過私有變量的key訪問其value。

意義:

  • 在類`Countdown`中,只能訪問存儲在`.#counter` 和 `#action` 的私有數據——因為在這個類中你只有這兩個私有變量。
  • 私有屬性不能被子類訪問。

6. 陷阱:使用`this`訪問私有靜態屬性

你可以使用`this`訪問公有靜態屬性,但是你不應該使用它訪問私有靜態屬性。

6.1 `this`和靜態公有變量

class SuperClass {
 static publicData = 1;
 
 static getPublicViaThis() {
 return this.publicData;
 }
}
class SubClass extends SuperClass {
}

公有靜態屬性(Public static fields)是屬性。如果我們用一個方法調用:

assert.equal(SuperClass.getPublicViaThis(), 1);

`this`指向`SuperClass`,一切都按照我們預想的那樣工作。我們也可以通過子類調用`.getPublicViaThis()`。

assert.equal(SubClass.getPublicViaThis(), 1);

`SubClass`繼承了`.getPublicViaThis()`方法,`this`指向`SubClass`,代碼依舊可以運用,因為`SubClass`也繼承了`.publicData`。

(注:在這種情況下設置`.publicData`會在`SubClass`上創建新的屬性,這個屬性不會覆蓋在`SuperClass`上定義的屬性)

6.2 `this`和私有靜態屬性

考慮這段代碼:

class SuperClass {
 static #privateData = 2;
 static getPrivateViaThis() {
 return this.#privateData;
 }
 static getPrivateViaClassName() {
 return SuperClass.#privateData;
 }
}
class SubClass extends SuperClass {
}

通過`SuperClass`可以調用`.getPrivateViaThis()`,因為`this`指向`SuperClass`:

assert.equal(SuperClass.getPrivateViaThis(), 2);

然而,通過`SubClass`無法調用`.getPrivateViaThis()`,因為此時`this`指向`SubClass`,而`SubClass`沒有私有靜態變量`.#privateData`:

assert.throws(
 () => SubClass.getPrivateViaThis(),
 {
 name: 'TypeError',
 message: 'Read of private field #privateData from' +
 ' an object which did not contain the field',
 }
);

解決辦法是直接通過`SuperClass`訪問`.#privateData`,就像`SuperClass`中的`getPrivateViaClassName`方法。

assert.equal(SubClass.getPrivateViaClassName(), 2);

7. “友好”和“被保護的”隱私

有時候我們想要某些實體成為某一類的“朋友”。這個朋友應該可以訪問calss的私有數據(譯者注:這裡其實就是想控制哪些可以訪問私有屬性,哪些不可以)。在以下代碼中,函數`getCounter()`是類`Countdown`的朋友。我們通過使用`WeakMaps`生成私有數據,這樣`Countdown`就允許朋友們訪問該數據。

const _counter = new WeakMap();
const _action = new WeakMap();
class Countdown {
 constructor(counter, action) {
 _counter.set(this, counter);
 _action.set(this, action);
 }
 dec() {
 let counter = _counter.get(this);
 if (counter < 1) return;
 counter--;
 _counter.set(this, counter);
 if (counter === 0) {
 _action.get(this)();
 }
 }
}
function getCounter(countdown) {
 return counter.get(countdown);
}

這樣就很容易控制哪些可以訪問私有數據:如果它們可以使用`_counter`和`_action`,它們就可以訪問私有數據。如果我們將前面的代碼片段放到一個模塊中,那麼數據在整個模塊中是私有的。

有關此技術的更多信息,可以查看Sect的“使用`WeakMaps`保持私有數據”。這同樣適用於在superclass和subclass之間共享私有數據(“被保護”的可見度)。

8. FAQ

8.1 為什麼是`#`?為什麼不通過`private`屬性聲明私有屬性?

原則上,私有屬性應該用如下方式聲明:

class MyClass {
 private value;
 compare(other) {
 return this.value === other.value;
 }
}

但是我們不能在整個class內的任何地方使用屬性名值——它們總是被解釋為私有變量。

靜態類型語言,例如TypeScript在這方面具有更多的靈活性:它們在編譯的時候就知道是否是MyClass的一個實例並且可以是否將`.value`作為一個私有變量對待。

key word

  • class私有屬性(private class fields)
  • 靜態私有屬性(private static fields)
  • 實例私有屬性(private instance fields)


分享到:


相關文章: