2021 年 6 月 18 日

私有和受保護的屬性和方法

物件導向程式最重要的原則之一,就是區分內部介面和外部介面。

這是開發任何比「hello world」應用程式更複雜的程式時「必須」遵循的原則。

為了理解這一點,讓我們暫時離開程式開發,轉而觀察真實世界。

我們使用的裝置通常都相當複雜。但是,區分內部介面和外部介面讓我們能夠在沒有問題的情況下使用這些裝置。

真實世界的範例

例如,咖啡機。從外部來看很簡單:一個按鈕、一個顯示器、幾個洞…當然,還有結果:美味的咖啡! :)

但內部...(維修手冊上的圖片)

很多細節。但我們可以在不知道任何事情的情況下使用它。

咖啡機相當可靠,不是嗎?我們可以使用它很多年,只有在出問題時才拿去修理。

咖啡機可靠且簡單的秘訣——所有細節都經過微調,並隱藏在內部。

如果我們從咖啡機上移除保護蓋,那麼使用它將會複雜得多(按哪裡?),而且很危險(可能會觸電)。

正如我們將看到的,在程式設計中,物件就像咖啡機。

但為了隱藏內部細節,我們不會使用保護蓋,而是使用語言和約定的特殊語法。

內部和外部介面

在物件導向程式設計中,屬性和方法分為兩組

  • 內部介面——方法和屬性,可以從類別的其他方法存取,但不能從外部存取。
  • 外部介面——方法和屬性,也可以從類別外部存取。

如果我們繼續類比咖啡機——隱藏在內部的是什麼:鍋爐管、加熱元件等——這是它的內部介面。

內部介面用於物件運作,其細節會互相使用。例如,鍋爐管連接到加熱元件。

但從外部來看,咖啡機被保護蓋封閉,這樣就沒有人可以接觸到它們。細節被隱藏且無法存取。我們可以透過外部介面使用其功能。

因此,我們使用物件所需要做的就是了解其外部介面。我們可能完全不知道它在內部如何運作,這很好。

這是一個概括性的介紹。

在 JavaScript 中,有兩種物件欄位(屬性和方法)

  • 公開:可以在任何地方存取。它們組成外部介面。到目前為止,我們只使用公開屬性和方法。
  • 私人:只能從類別內部存取。這些是供內部介面使用。

在許多其他語言中,也存在「受保護」欄位:只能從類別內部和擴充它的類別存取(就像私人,但加上從繼承類別存取的權限)。它們也對內部介面很有用。它們在某種意義上比私人欄位更廣泛,因為我們通常希望繼承類別可以存取它們。

受保護的欄位並未在 JavaScript 中的語言層級中實作,但實際上它們非常方便,因此會模擬它們。

現在我們將使用所有這些類型的屬性在 JavaScript 中製作咖啡機。咖啡機有很多細節,我們不會對它們進行建模以保持簡單(儘管我們可以)。

保護「waterAmount」

讓我們先製作一個簡單的咖啡機類別

class CoffeeMachine {
  waterAmount = 0; // the amount of water inside

  constructor(power) {
    this.power = power;
    alert( `Created a coffee-machine, power: ${power}` );
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = 200;

現在,屬性 waterAmountpower 是公開的。我們可以輕鬆地從外部取得/設定它們為任何值。

讓我們將 waterAmount 屬性變更為受保護,以對其有更多控制權。例如,我們不希望任何人將其設定為低於零。

受保護的屬性通常以底線 _ 為前綴。

這並未在語言層級中強制執行,但程式設計師之間有一個眾所周知的慣例,即不應從外部存取此類屬性和方法。

因此,我們的屬性將稱為 _waterAmount

class CoffeeMachine {
  _waterAmount = 0;

  set waterAmount(value) {
    if (value < 0) {
      value = 0;
    }
    this._waterAmount = value;
  }

  get waterAmount() {
    return this._waterAmount;
  }

  constructor(power) {
    this._power = power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

// add water
coffeeMachine.waterAmount = -10; // _waterAmount will become 0, not -10

現在存取受到控制,因此將水量設定為低於零變得不可能。

唯讀「power」

對於 power 屬性,讓我們使其為唯讀。有時會發生必須僅在建立時設定屬性,然後永遠不修改的情況。

這正是咖啡機的情況:電源永遠不會改變。

為此,我們只需要建立 getter,而不是 setter

class CoffeeMachine {
  // ...

  constructor(power) {
    this._power = power;
  }

  get power() {
    return this._power;
  }

}

// create the coffee machine
let coffeeMachine = new CoffeeMachine(100);

alert(`Power is: ${coffeeMachine.power}W`); // Power is: 100W

coffeeMachine.power = 25; // Error (no setter)
getter/setter 函式

在這裡,我們使用了 getter/setter 語法。

但大多數時候,會優先使用 get.../set... 函式,如下所示

class CoffeeMachine {
  _waterAmount = 0;

  setWaterAmount(value) {
    if (value < 0) value = 0;
    this._waterAmount = value;
  }

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

這樣看起來有點長,但函式更靈活。它們可以接受多個引數(即使我們現在不需要它們)。

另一方面,get/set 語法較短,因此最終沒有嚴格的規則,由您決定。

受保護的欄位會繼承

如果我們繼承 class MegaMachine extends CoffeeMachine,那麼沒有什麼可以阻止我們從新類別的方法中存取 this._waterAmountthis._power

因此,受保護的欄位自然是可以繼承的。這與我們下面將看到的私有欄位不同。

私有的「#waterLimit」

最近新增
這是最近新增到語言中的內容。在 JavaScript 引擎中尚未支援,或部分支援,需要 polyfilling

有一個已完成的 JavaScript 提案,幾乎符合標準,它提供語言層級的支援,用於私有屬性和方法。

私有屬性應以 # 開頭。它們只能從類別內部存取。

例如,以下是私有的 #waterLimit 屬性和檢查水的私有方法 #fixWaterAmount

class CoffeeMachine {
  #waterLimit = 200;

  #fixWaterAmount(value) {
    if (value < 0) return 0;
    if (value > this.#waterLimit) return this.#waterLimit;
  }

  setWaterAmount(value) {
    this.#waterLimit = this.#fixWaterAmount(value);
  }

}

let coffeeMachine = new CoffeeMachine();

// can't access privates from outside of the class
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

在語言層級上,# 是欄位為私有的特殊符號。我們無法從外部或繼承類別存取它。

私有欄位不會與公開欄位衝突。我們可以同時擁有私有的 #waterAmount 和公開的 waterAmount 欄位。

例如,我們將 waterAmount 設為 #waterAmount 的存取器

class CoffeeMachine {

  #waterAmount = 0;

  get waterAmount() {
    return this.#waterAmount;
  }

  set waterAmount(value) {
    if (value < 0) value = 0;
    this.#waterAmount = value;
  }
}

let machine = new CoffeeMachine();

machine.waterAmount = 100;
alert(machine.#waterAmount); // Error

與受保護的欄位不同,私有欄位是由語言本身強制執行的。這是一件好事。

但是,如果我們從 CoffeeMachine 繼承,那麼我們將無法直接存取 #waterAmount。我們需要依賴 waterAmount getter/setter

class MegaCoffeeMachine extends CoffeeMachine {
  method() {
    alert( this.#waterAmount ); // Error: can only access from CoffeeMachine
  }
}

在許多情況下,這種限制太過嚴格。如果我們擴充 CoffeeMachine,我們可能會有正當理由存取其內部。這就是為什麼受保護的欄位更常被使用,即使它們不受語言語法支援。

私有欄位無法作為 this[name]

私有欄位很特別。

如我們所知,我們通常可以使用 this[name] 存取欄位

class User {
  ...
  sayHi() {
    let fieldName = "name";
    alert(`Hello, ${this[fieldName]}`);
  }
}

對於私有欄位,這是不可行的:this['#name'] 無法運作。這是一個語法限制,用於確保私密性。

摘要

在 OOP 中,將內部介面與外部介面區分開來稱為 封裝

它提供了以下好處

保護使用者,以免他們自討苦吃

想像一下,有一群開發人員使用咖啡機。它是由「最佳咖啡機」公司製造的,運作良好,但保護蓋被移除了。因此,內部介面是公開的。

所有開發人員都很文明 - 他們按照預期使用咖啡機。但其中一位,約翰,認為自己是聰明人,並對咖啡機的內部進行了一些調整。因此,咖啡機在兩天後故障了。

這肯定不是約翰的錯,而是移除保護蓋並讓約翰進行操作的人的錯。

程式設計中也是如此。如果類別的使用者變更了不打算從外部變更的內容 - 後果是無法預測的。

可支援

程式設計中的情況比實際的咖啡機更複雜,因為我們不只是購買一次。程式碼會持續開發和改進。

如果我們嚴格區分內部介面,那麼類別的開發人員可以自由變更其內部屬性和方法,甚至無需通知使用者。

如果您是此類別的開發人員,很高兴知道私有方法可以安全地重新命名,它們的參數可以變更,甚至可以移除,因為沒有外部程式碼依賴它們。

對於使用者而言,當新版本推出時,內部可能進行了全面檢修,但如果外部介面相同,升級仍然很簡單。

隱藏複雜性

人們喜歡使用簡單的東西。至少從外表上來看是如此。內部是什麼又是另一回事了。

程式設計師也不例外。

當實作細節被隱藏起來,並提供一個簡單、文件齊全的外部介面時,總是會很方便。

為了隱藏內部介面,我們使用受保護或私有的屬性

  • 受保護的欄位以 _ 開頭。這是一個眾所周知的慣例,並未在語言層級強制執行。程式設計師只能從其類別和繼承自其類別的類別存取以 _ 開頭的欄位。
  • 私有欄位以 # 開頭。JavaScript 確保我們只能從類別內部存取這些欄位。

目前,私有欄位在瀏覽器之間並不支援良好,但可以使用 polyfill。

教學課程地圖

留言

留言前請先閱讀以下內容…
  • 如果您有改進建議,請 提交 GitHub 問題 或發起 pull request,而不是留言。
  • 如果您無法理解文章中的某個部分,請詳細說明。
  • 若要插入幾行程式碼,請使用 <code> 標籤;若要插入多行程式碼,請將它們包覆在 <pre> 標籤中;若要插入超過 10 行的程式碼,請使用沙盒 (plnkrjsbincodepen…)