面向物件程式設計
面向物件程式設計(OOP)是一種程式設計正規化,是許多程式語言(包括 Java 和 C++)的基礎。本文將概述 OOP 的基本概念。我們將介紹三個主要概念:**類和例項**、**繼承**和**封裝**。目前,我們將描述這些概念,不特別參考 JavaScript,因此所有示例都使用虛擬碼給出。
注意:準確地說,此處描述的功能屬於一種稱為**基於類**或“經典”OOP 的特定 OOP 風格。當人們談論 OOP 時,通常指的就是這種型別。
之後,在 JavaScript 中,我們將瞭解建構函式和原型鏈如何與這些 OOP 概念相關聯,以及它們之間的區別。在下一篇文章中,我們將介紹 JavaScript 的一些其他功能,這些功能使實現面向物件程式變得更容易。
| 先決條件 | 瞭解 JavaScript 函式,熟悉 JavaScript 基礎知識(參見第一步和構建塊),以及 OOJS 基礎知識(參見物件簡介和物件原型)。 |
|---|---|
| 目標 | 理解基於類的面向物件程式設計的基本概念。 |
面向物件程式設計是關於將系統建模為物件的集合,其中每個物件代表系統的某個特定方面。物件包含函式(或方法)和資料。物件為想要使用它的其他程式碼提供公共介面,但同時維護自己的私有內部狀態;系統其他部分不必關心物件內部發生了什麼。
類和例項
當我們在 OOP 中用物件來建模問題時,我們會建立抽象定義來表示我們希望在系統中擁有的物件型別。例如,如果我們正在對學校進行建模,我們可能希望擁有表示教授的物件。每個教授都有一些共同的屬性:他們都有姓名和教授的科目。此外,每個教授都可以做某些事情:例如,他們都可以批改作業,並在學年開始時向學生介紹自己。
所以Professor可以在我們的系統中成為一個類。類的定義列出了每個教授擁有的資料和方法。
在虛擬碼中,Professor類可以這樣寫
class Professor
properties
name
teaches
methods
grade(paper)
introduceSelf()
這定義了一個具有以下屬性的Professor類:
- 兩個資料屬性:
name和teaches - 兩個方法:
grade()用於批改作業,introduceSelf()用於自我介紹。
類本身不做任何事情:它是一種建立該型別具體物件的模板。我們建立的每個具體教授都稱為Professor類的例項。建立例項的過程由一個稱為建構函式的特殊函式執行。我們向建構函式傳遞任何我們希望在新例項中初始化的內部狀態的值。
通常,建構函式作為類定義的一部分寫出,並且通常與類本身具有相同的名稱
class Professor
properties
name
teaches
constructor
Professor(name, teaches)
methods
grade(paper)
introduceSelf()
此建構函式採用兩個引數,因此我們可以在建立新的具體教授時初始化name和teaches屬性。
現在我們有了建構函式,我們可以建立一些教授了。程式語言通常使用關鍵字new來表示正在呼叫建構函式。
walsh = new Professor("Walsh", "Psychology");
lillian = new Professor("Lillian", "Poetry");
walsh.teaches; // 'Psychology'
walsh.introduceSelf(); // 'My name is Professor Walsh and I will be your Psychology professor.'
lillian.teaches; // 'Poetry'
lillian.introduceSelf(); // 'My name is Professor Lillian and I will be your Poetry professor.'
這建立了兩個物件,它們都是Professor類的例項。
繼承
假設在我們的學校中,我們還想表示學生。與教授不同,學生不能批改作業,不教授特定科目,並且屬於特定年級。
但是,學生確實有姓名,也可能希望自我介紹,因此我們可以這樣寫出學生類的定義
class Student
properties
name
year
constructor
Student(name, year)
methods
introduceSelf()
如果我們能夠表示學生和教授共享某些屬性,或者更準確地說,表示他們在某種程度上是“相同型別的事物”,那就很有幫助了。繼承讓我們可以做到這一點。
我們首先觀察到學生和教授都是人,人都具有姓名並希望自我介紹。我們可以透過定義一個新的類Person來模擬這一點,在其中我們定義了所有人的共同屬性。然後,Professor和Student都可以從Person派生,並新增它們的額外屬性
class Person
properties
name
constructor
Person(name)
methods
introduceSelf()
class Professor : extends Person
properties
teaches
constructor
Professor(name, teaches)
methods
grade(paper)
introduceSelf()
class Student : extends Person
properties
year
constructor
Student(name, year)
methods
introduceSelf()
在這種情況下,我們會說Person是Professor和Student的超類或父類。相反,Professor和Student是Person的子類或子類。
您可能會注意到introduceSelf()在所有三個類中都已定義。這樣做的原因是,雖然所有人都希望自我介紹,但他們的介紹方式卻有所不同
walsh = new Professor("Walsh", "Psychology");
walsh.introduceSelf(); // 'My name is Professor Walsh and I will be your Psychology professor.'
summers = new Student("Summers", 1);
summers.introduceSelf(); // 'My name is Summers and I'm in the first year.'
我們可能有一個針對不是學生或教授的人的introduceSelf()的預設實現
pratt = new Person("Pratt");
pratt.introduceSelf(); // 'My name is Pratt.'
當方法具有相同的名稱但在不同的類中具有不同的實現時,此功能稱為多型性。當子類中的方法替換超類的實現時,我們說子類覆蓋了超類中的版本。
封裝
物件為想要使用它們的程式碼提供介面,但同時維護自己的內部狀態。物件的內部狀態保持私有,這意味著它只能被物件自己的方法訪問,而不能被其他物件訪問。保持物件的內部狀態私有,並且通常在物件的公共介面和其私有內部狀態之間進行清晰的劃分,這稱為封裝。
這是一個有用的功能,因為它使程式設計師能夠更改物件的內部實現,而無需查詢和更新所有使用它的程式碼:它在該物件與系統其餘部分之間建立了一種“防火牆”。
例如,假設學生如果在二年級或以上,則可以學習射箭。我們只需公開學生的year屬性,其他程式碼可以檢查該屬性以確定學生是否可以選修該課程
if (student.year > 1) {
// allow the student into the class
}
問題是,如果我們決定更改允許學生學習射箭的標準——例如,還要求父母或監護人同意——我們需要更新系統中執行此測試的每個位置。最好在Student物件上有一個canStudyArchery()方法,該方法在一個地方實現邏輯
class Student : extends Person
properties
year
constructor
Student(name, year)
methods
introduceSelf()
canStudyArchery() { return this.year > 1 }
if (student.canStudyArchery()) {
// allow the student into the class
}
這樣,如果我們想更改學習射箭的規則,我們只需要更新Student類,並且所有使用它的程式碼仍然可以工作。
在許多 OOP 語言中,我們可以透過將某些屬性標記為private來阻止其他程式碼訪問物件的內部狀態。如果物件外部的程式碼嘗試訪問它們,這將生成錯誤
class Student : extends Person
properties
private year
constructor
Student(name, year)
methods
introduceSelf()
canStudyArchery() { return this.year > 1 }
student = new Student('Weber', 1)
student.year // error: 'year' is a private property of Student
在不強制執行此類訪問的語言中,程式設計師使用命名約定(例如,以下劃線開頭名稱)來指示應將該屬性視為私有。
OOP 和 JavaScript
在本文中,我們描述了基於類的面向物件程式設計的一些基本功能,這些功能在 Java 和 C++ 等語言中實現。
在前面兩篇文章中,我們瞭解了兩個核心 JavaScript 功能:建構函式和原型。這些功能當然與上面描述的一些 OOP 概念有一定關係。
- JavaScript 中的建構函式為我們提供了類似於類定義的東西,使我們能夠在一個地方定義物件的“形狀”,包括它包含的任何方法。但原型也可以在這裡使用。例如,如果在建構函式的
prototype屬性上定義了一個方法,那麼使用該建構函式建立的所有物件都透過其原型獲得該方法,我們不需要在建構函式中定義它。 - 原型鏈似乎是實現繼承的自然方式。例如,如果我們可以擁有一個原型為
Person的Student物件,那麼它可以繼承name並覆蓋introduceSelf()。
但值得了解這些功能與上面描述的“經典”OOP 概念之間的區別。我們將在本文中重點介紹其中的一些。
首先,在基於類的 OOP 中,類和物件是兩個獨立的構造,並且物件始終作為類的例項建立。此外,定義類(類語法本身)的功能與例項化物件(建構函式)的功能之間存在區別。在 JavaScript 中,我們可以並且經常在沒有任何單獨的類定義的情況下建立物件,無論是使用函式還是物件文字。這使得處理物件比在經典 OOP 中輕量級得多。
其次,儘管原型鏈看起來像繼承層次結構,並且在某些方面表現得像繼承層次結構,但在其他方面卻有所不同。當例項化子類時,會建立一個單個物件,該物件將子類中定義的屬性與層次結構中更高級別定義的屬性組合在一起。使用原型,層次結構的每個級別都由一個單獨的物件表示,並且它們透過__proto__屬性連結在一起。原型鏈的行為更像委託而不是繼承。委託是一種程式設計模式,其中一個物件在被要求執行任務時,可以自己執行任務或要求另一個物件(其**委託**)代表它執行任務。在許多方面,委託比繼承是一種更靈活的物件組合方式(例如,可以在執行時更改或完全替換委託)。
也就是說,建構函式和原型可以用來在 JavaScript 中實現基於類的 OOP 模式。但是直接使用它們來實現諸如繼承之類的功能非常棘手,因此 JavaScript 提供了額外的功能,這些功能構建在原型模型之上,更直接地對映到基於類的 OOP 的概念。這些額外功能是下一篇文章的主題。