面向物件程式設計
面向物件程式設計 (OOP) 是一種程式設計正規化,它是許多程式語言(包括 Java 和 C++)的基礎。本文將概述 OOP 的基本概念。我們將描述三個主要概念:類和例項、繼承和封裝。目前,我們將不特別參考 JavaScript 來描述這些概念,因此所有示例都使用虛擬碼給出。
注意:準確地說,這裡描述的特性是一種特定風格的 OOP,稱為基於類的或“經典”OOP。當人們談論 OOP 時,通常指的就是這種型別。
之後,在 JavaScript 中,我們將探討建構函式和原型鏈如何與這些 OOP 概念相關聯,以及它們之間的差異。在下一篇文章中,我們將探討 JavaScript 中一些額外的特性,這些特性使得實現面向物件程式變得更加容易。
| 預備知識 | 熟悉 JavaScript 基礎(尤其是物件基礎)和本模組先前課程中涵蓋的面向物件 JavaScript 概念。 |
|---|---|
| 學習成果 |
|
面向物件程式設計是將系統建模為物件的集合,其中每個物件代表系統的某個特定方面。物件既包含函式(或方法)又包含資料。物件向其他想要使用它的程式碼提供公共介面,但維護自己的私有內部狀態;系統的其他部分不必關心物件內部發生了什麼。
類和例項
當我們用 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 概念。這些額外功能是下一篇文章的主題。
總結
本文描述了基於類的面向物件程式設計的基本特性,並簡要介紹了 JavaScript 建構函式和原型與這些概念的比較。
在下一篇文章中,我們將探討 JavaScript 為支援基於類的面向物件程式設計而提供的功能。