面向對象編程最重要的原則之壹 —— 將內部接口與外部接口分隔開來。
在開發比 “hello world” 應用程序更複雜的東西時,這是“必須”遵守的做法。
爲了理解這壹點,讓我們脫離開發過程,把目光轉向現實世界。
通常,我們使用的設備都非常複雜。但是,將內部接口與外部接口分隔開來可以讓我們使用它們且沒有任何問題。
例如,壹個咖啡機。從外面看很簡單:壹個按鈕,壹個顯示器,幾個洞……當然,結果就是 —— 很棒的咖啡!:)
但是在內部……(壹張摘自維修手冊的圖片)
有非常多的細節。但我們可以在完全不了解這些內部細節的情況下使用它。
咖啡機非常可靠,不是嗎?壹台咖啡機我們可以使用好幾年,只有在出現問題時 —— 把它送去維修。
咖啡機的可靠性和簡潔性的秘訣 —— 所有細節都經過精心校並 隱藏 在內部。
如果我們從咖啡機上取下保護罩,那麽使用它將變得複雜得多(要按哪裏?),並且很危險(會觸電)。
正如我們所看到的,在編程中,對象就像咖啡機。
但是爲了隱藏內部細節,我們不會使用保護罩,而是使用語言和約定中的特殊語法。
在面向對象的編程中,屬性和方法分爲兩組:
內部接口 —— 可以通過該類的其他方法訪問,但不能從外部訪問的方法和屬性。
外部接口 —— 也可以從類的外部訪問的方法和屬性。
如果我們繼續用咖啡機進行類比 —— 內部隱藏的內容:鍋爐管,加熱元件等 —— 是咖啡機的內部接口。
內部接口用于對象工作,它的細節相互使用。例如,鍋爐管連接到加熱元件。
但是從外面看,壹台咖啡機被保護殼罩住了,所以沒有人可以接觸到其內部接口。細節信息被隱藏起來並且無法訪問。我們可以通過外部接口使用它的功能。
所以,我們需要使用壹個對象時只需知道它的外部接口。我們可能完全不知道它的內部是如何工作的,這太好了。
這是個概括性的介紹。
在 JavaScript 中,有兩種類型的對象字段(屬性和方法):
公共的:可從任何地方訪問。它們構成了外部接口。到目前爲止,我們只使用了公共的屬性和方法。
私有的:只能從類的內部訪問。這些用于內部接口。
在許多其他編程語言中,還存在“受保護”的字段:只能從類的內部和基于其擴展的類的內部訪問(例如私有的,但可以從繼承的類進行訪問)。它們對于內部接口也很有用。從某種意義上講,它們比私有的屬性和方法更爲廣泛,因爲我們通常希望繼承類來訪問它們。
受保護的字段不是在語言級別的 JavaScript 中實現的,但實際上它們非常方便,因爲它們是在 JavaScript 中模擬的類定義語法。
現在,我們將使用所有這些類型的屬性在 JavaScript 中制作咖啡機。咖啡機有很多細節,我們不會對它們進行全面模擬以保持簡潔(盡管我們可以)。
首先,讓我們做壹個簡單的咖啡機類:
class CoffeeMachine { waterAmount = 0; // 內部的水量 constructor(power) { this.power = power; alert( `Created a coffee-machine, power: ${power}` ); } } // 創建咖啡機 let coffeeMachine = new CoffeeMachine(100); // 加水 coffeeMachine.waterAmount = 200;
現在,屬性 waterAmount
和 power
是公共的。我們可以輕松地從外部將它們 get/set 成任何值。
讓我們將 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; } } // 創建咖啡機 let coffeeMachine = new CoffeeMachine(100); // 加水 coffeeMachine.waterAmount = -10; // _waterAmount 將變爲 0,而不是 -10
現在訪問已受到控制,因此將水量的值設置爲小于零的數變得不可能。
對于 power
屬性,讓我們將它設爲只讀。有時候壹個屬性必須只能被在創建時進行設置,之後不再被修改。
咖啡機就是這種情況:功率永遠不會改變。
要做到這壹點,我們只需要設置 getter,而不設置 setter:
class CoffeeMachine { // ... constructor(power) { this._power = power; } get power() { return this._power; } } // 創建咖啡機 let coffeeMachine = new CoffeeMachine(100); alert(`Power is: ${coffeeMachine.power}W`); // 功率是:100W coffeeMachine.power = 25; // Error(沒有 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._waterAmount
或 this._power
。
所以受保護的字段是自然可被繼承的。與我們接下來將看到的私有字段不同。
最近新增的特性
這是壹個最近添加到 JavaScript 的特性。 JavaScript 引擎不支持(或部分支持),需要 polyfills。
這兒有壹個馬上就會被加到規範中的已完成的 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
在語言級別,#
是該字段爲私有的特殊標志。我們無法從外部或從繼承的類中訪問它。
私有字段與公共字段不會發生沖突。我們可以同時擁有私有的 #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)而言,內部接口與外部接口的劃分被稱爲 封裝。
它具有以下優點:
保護用戶,使他們不會誤傷自己
想象壹下,有壹群開發人員在使用壹個咖啡機。這個咖啡機是由“最好的咖啡機”公司制造的,工作正常,但是保護罩被拿掉了。因此內部接口暴露了出來。
所有的開發人員都是文明的 —— 他們按照預期使用咖啡機。但其中的壹個人,約翰,他認爲自己是最聰明的人,並對咖啡機的內部做了壹些調整。然而,咖啡機兩天後就壞了。
這肯定不是約翰的錯,而是那個取下保護罩並讓約翰進行操作的人的錯。
編程也壹樣。如果壹個 class 的使用者想要改變那些本不打算被從外部更改的東西 —— 後果是不可預測的。
可支持性
編程的情況比現實生活中的咖啡機要複雜得多,因爲我們不只是購買壹次。我們還需要不斷開發和改進代碼。
如果我們嚴格界定內部接口,那麽這個 class 的開發人員可以自由地更改其內部屬性和方法,甚至無需通知用戶。
如果妳是這樣的 class 的開發者,那麽妳會很高興知道可以安全地重命名私有變量,可以更改甚至刪除其參數,因爲沒有外部代碼依賴于它們。
對于用戶來說,當新版本問世時,應用的內部可能被進行了全面檢修,但如果外部接口相同,則仍然很容易升級。
隱藏複雜性
人們喜歡使用簡單的東西。至少從外部來看是這樣。內部的東西則是另外壹回事了。
程序員也不例外。
當實施細節被隱藏,並提供了簡單且有據可查的外部接口時,總是很方便的。
爲了隱藏內部接口,我們使用受保護的或私有的屬性:
受保護的字段以 _
開頭。這是壹個衆所周知的約定,不是在語言級別強制執行的。程序員應該只通過它的類和從它繼承的類中訪問以 _
開頭的字段。
私有字段以 #
開頭。JavaScript 確保我們只能從類的內部訪問它們。
目前,各個浏覽器對私有字段的支持不是很好,但可以用 polyfill 解決。