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

2023-02-17 10:52 更新

面向對象編程最重要的原則之一 —— 將內部接口與外部接口分隔開來。

在開發(fā)比 “hello world” 應用程序更復雜的東西時,這是“必須”遵守的做法。

為了理解這一點,讓我們脫離開發(fā)過程,把目光轉向現(xiàn)實世界。

通常,我們使用的設備都非常復雜。但是,將內部接口與外部接口分隔開來可以讓我們使用它們且沒有任何問題。

一個現(xiàn)實生活中的例子

例如,一個咖啡機。從外面看很簡單:一個按鈕,一個顯示器,幾個洞……當然,結果就是 —— 很棒的咖啡!:)


但是在內部……(一張摘自維修手冊的圖片)


有非常多的細節(jié)。但我們可以在完全不了解這些內部細節(jié)的情況下使用它。

咖啡機非??煽?,不是嗎?一臺咖啡機我們可以使用好幾年,只有在出現(xiàn)問題時 —— 把它送去維修。

咖啡機的可靠性和簡潔性的秘訣 —— 所有細節(jié)都經(jīng)過精心校并 隱藏 在內部。

如果我們從咖啡機上取下保護罩,那么使用它將變得復雜得多(要按哪里?),并且很危險(會觸電)。

正如我們所看到的,在編程中,對象就像咖啡機。

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

內部接口和外部接口

在面向對象的編程中,屬性和方法分為兩組:

  • 內部接口 —— 可以通過該類的其他方法訪問,但不能從外部訪問的方法和屬性。
  • 外部接口 —— 也可以從類的外部訪問的方法和屬性。

如果我們繼續(xù)用咖啡機進行類比 —— 內部隱藏的內容:鍋爐管,加熱元件等 —— 是咖啡機的內部接口。

內部接口用于對象工作,它的細節(jié)相互使用。例如,鍋爐管連接到加熱元件。

但是從外面看,一臺咖啡機被保護殼罩住了,所以沒有人可以接觸到其內部接口。細節(jié)信息被隱藏起來并且無法訪問。我們可以通過外部接口使用它的功能。

所以,我們需要使用一個對象時只需知道它的外部接口。我們可能完全不知道它的內部是如何工作的,這太好了。

這是個概括性的介紹。

在 JavaScript 中,有兩種類型的對象字段(屬性和方法):

  • 公共的:可從任何地方訪問。它們構成了外部接口。到目前為止,我們只使用了公共的屬性和方法。
  • 私有的:只能從類的內部訪問。這些用于內部接口。

在許多其他編程語言中,還存在“受保護”的字段:只能從類的內部和基于其擴展的類的內部訪問(例如私有的,但可以從繼承的類進行訪問)。它們對于內部接口也很有用。從某種意義上講,它們比私有的屬性和方法更為廣泛,因為我們通常希望繼承類來訪問它們。

受保護的字段不是在語言級別的 Javascript 中實現(xiàn)的,但實際上它們非常方便,因為它們是在 Javascript 中模擬的類定義語法。

現(xiàn)在,我們將使用所有這些類型的屬性在 Javascript 中制作咖啡機??Х葯C有很多細節(jié),我們不會對它們進行全面模擬以保持簡潔(盡管我們可以)。

受保護的 “waterAmount”

首先,讓我們做一個簡單的咖啡機類:

class CoffeeMachine {
  waterAmount = 0; // 內部的水量

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

}

// 創(chuàng)建咖啡機
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = 200;

現(xiàn)在,屬性 waterAmount 和 power 是公共的。我們可以輕松地從外部將它們 get/set 成任何值。

讓我們將 waterAmount 屬性更改為受保護的屬性,以對其進行更多控制。例如,我們不希望任何人將它的值設置為小于零的數(shù)。

受保護的屬性通常以下劃線 _ 作為前綴。

這不是在語言級別強制實施的,但是程序員之間有一個眾所周知的約定,即不應該從外部訪問此類型的屬性和方法。

所以我們的屬性將被命名為 _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;
  }

}

// 創(chuàng)建咖啡機
let coffeeMachine = new CoffeeMachine(100);

// 加水
coffeeMachine.waterAmount = -10; // _waterAmount 將變?yōu)?0,而不是 -10

現(xiàn)在訪問已受到控制,因此將水量的值設置為小于零的數(shù)變得不可能。

只讀的 “power”

對于 power 屬性,讓我們將它設為只讀。有時候一個屬性必須只能被在創(chuàng)建時進行設置,之后不再被修改。

咖啡機就是這種情況:功率永遠不會改變。

要做到這一點,我們只需要設置 getter,而不設置 setter:

class CoffeeMachine {
  // ...

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

  get power() {
    return this._power;
  }

}

// 創(chuàng)建咖啡機
let coffeeMachine = new CoffeeMachine(100);

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

coffeeMachine.power = 25; // Error(沒有 setter)

getter/setter 函數(shù)

這里我們使用了 getter/setter 語法。

但大多數(shù)時候首選 ?get.../set...? 函數(shù),像這樣:

class CoffeeMachine {
  _waterAmount = 0;

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

  getWaterAmount() {
    return this._waterAmount;
  }
}

new CoffeeMachine().setWaterAmount(100);

這看起來有點長,但函數(shù)更靈活。它們可以接受多個參數(shù)(即使我們現(xiàn)在還不需要)。

另一方面,get/set 語法更短,所以最終沒有嚴格的規(guī)定,而是由你自己來決定。

受保護的字段是可以被繼承的

如果我們繼承 class MegaMachine extends CoffeeMachine,那么什么都無法阻止我們從新的類中的方法訪問 this._waterAmount 或 this._power

所以受保護的字段是自然可被繼承的。與我們接下來將看到的私有字段不同。

私有的 “#waterLimit”

最近新增的特性

這是一個最近添加到 JavaScript 的特性。 JavaScript 引擎不支持(或部分支持),需要 polyfills。

這兒有一個馬上就會被加到規(guī)范中的已完成的 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();

// 不能從類的外部訪問類的私有屬性和方法
coffeeMachine.#fixWaterAmount(123); // Error
coffeeMachine.#waterLimit = 1000; // Error

在語言級別,# 是該字段為私有的特殊標志。我們無法從外部或從繼承的類中訪問它。

私有字段與公共字段不會發(fā)生沖突。我們可以同時擁有私有的 #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

與受保護的字段不同,私有字段由語言本身強制執(zhí)行。這是好事兒。

但是如果我們繼承自 CoffeeMachine,那么我們將無法直接訪問 #waterAmount。我們需要依靠 waterAmount getter/setter:

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

在許多情況下,這種限制太嚴重了。如果我們擴展 CoffeeMachine,則可能有正當理由訪問其內部。這就是為什么大多數(shù)時候都會使用受保護字段,即使它們不受語言語法的支持。

私有字段不能通過 this[name] 訪問

私有字段很特別。

正如我們所知道的,通常我們可以使用 ?this[name]? 訪問字段:

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

對于私有字段來說,這是不可能的:this['#name'] 不起作用。這是確保私有性的語法限制。

總結

就面向對象編程(OOP)而言,內部接口與外部接口的劃分被稱為 封裝。

它具有以下優(yōu)點:

保護用戶,使他們不會誤傷自己

想象一下,有一群開發(fā)人員在使用一個咖啡機。這個咖啡機是由“最好的咖啡機”公司制造的,工作正常,但是保護罩被拿掉了。因此內部接口暴露了出來。

所有的開發(fā)人員都是文明的 —— 他們按照預期使用咖啡機。但其中的一個人,約翰,他認為自己是最聰明的人,并對咖啡機的內部做了一些調整。然而,咖啡機兩天后就壞了。

這肯定不是約翰的錯,而是那個取下保護罩并讓約翰進行操作的人的錯。

編程也一樣。如果一個 class 的使用者想要改變那些本不打算被從外部更改的東西 —— 后果是不可預測的。

可支持性

編程的情況比現(xiàn)實生活中的咖啡機要復雜得多,因為我們不只是購買一次。我們還需要不斷開發(fā)和改進代碼。

如果我們嚴格界定內部接口,那么這個 class 的開發(fā)人員可以自由地更改其內部屬性和方法,甚至無需通知用戶。

如果你是這樣的 class 的開發(fā)者,那么你會很高興知道可以安全地重命名私有變量,可以更改甚至刪除其參數(shù),因為沒有外部代碼依賴于它們。

對于用戶來說,當新版本問世時,應用的內部可能被進行了全面檢修,但如果外部接口相同,則仍然很容易升級。

隱藏復雜性

人們喜歡使用簡單的東西。至少從外部來看是這樣。內部的東西則是另外一回事了。

程序員也不例外。

當實施細節(jié)被隱藏,并提供了簡單且有據(jù)可查的外部接口時,總是很方便的。

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

  • 受保護的字段以 ?_? 開頭。這是一個眾所周知的約定,不是在語言級別強制執(zhí)行的。程序員應該只通過它的類和從它繼承的類中訪問以 ?_? 開頭的字段。
  • 私有字段以 ?#? 開頭。JavaScript 確保我們只能從類的內部訪問它們。

目前,各個瀏覽器對私有字段的支持不是很好,但可以用 polyfill 解決。


以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號