面向物件程式設計

面向物件程式設計 (OOP) 是一種程式設計正規化,它是許多程式語言(包括 Java 和 C++)的基礎。本文將概述 OOP 的基本概念。我們將描述三個主要概念:類和例項繼承封裝。目前,我們將不特別參考 JavaScript 來描述這些概念,因此所有示例都使用虛擬碼給出。

注意:準確地說,這裡描述的特性是一種特定風格的 OOP,稱為基於類的或“經典”OOP。當人們談論 OOP 時,通常指的就是這種型別。

之後,在 JavaScript 中,我們將探討建構函式和原型鏈如何與這些 OOP 概念相關聯,以及它們之間的差異。在下一篇文章中,我們將探討 JavaScript 中一些額外的特性,這些特性使得實現面向物件程式變得更加容易。

預備知識 熟悉 JavaScript 基礎(尤其是物件基礎)和本模組先前課程中涵蓋的面向物件 JavaScript 概念。
學習成果
  • 面向物件程式設計 (OOP) 概念:類、例項、繼承和封裝。
  • 這些 OOP 概念如何應用於 JavaScript,以及它與 Java 或 C++ 等語言之間的差異。

面向物件程式設計是將系統建模為物件的集合,其中每個物件代表系統的某個特定方面。物件既包含函式(或方法)又包含資料。物件向其他想要使用它的程式碼提供公共介面,但維護自己的私有內部狀態;系統的其他部分不必關心物件內部發生了什麼。

類和例項

當我們用 OOP 中的物件來建模問題時,我們建立抽象定義來表示我們希望在系統中擁有的物件型別。例如,如果我們正在模擬一所學校,我們可能希望有代表教授的物件。每個教授都有一些共同的屬性:他們都有一個名字和一門他們教授的科目。此外,每個教授都可以做某些事情:他們都可以批改論文,並且在學年開始時向學生介紹自己。

因此,Professor 可以是我們系統中的一個。類的定義列出了每個教授擁有的資料和方法。

在虛擬碼中,Professor 類可以這樣編寫

class Professor
    properties
        name
        teaches
    methods
        grade(paper)
        introduceSelf()

這定義了一個 Professor 類,具有

  • 兩個資料屬性:nameteaches
  • 兩個方法:grade() 用於批改論文,introduceSelf() 用於自我介紹。

類本身不執行任何操作:它是一種建立該型別具體物件的模板。我們建立的每個具體教授都稱為 Professor 類的例項。建立例項的過程由一個特殊函式執行,該函式稱為建構函式。我們將值傳遞給建構函式,用於在新例項中初始化任何內部狀態。

通常,建構函式作為類定義的一部分編寫,並且通常與類本身同名

class Professor
    properties
        name
        teaches
    constructor
        Professor(name, teaches)
    methods
        grade(paper)
        introduceSelf()

這個建構函式接受兩個引數,因此我們可以在建立新的具體教授時初始化 nameteaches 屬性。

現在我們有了一個建構函式,我們可以建立一些教授。程式語言通常使用關鍵字 new 來表示正在呼叫建構函式。

js
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 來建模這一點,在該類中我們定義了人的所有共同屬性。然後,ProfessorStudent 都可以從 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()

在這種情況下,我們會說 PersonProfessorStudent超類父類。相反,ProfessorStudentPerson子類

你可能會注意到 introduceSelf() 在所有三個類中都有定義。原因是雖然所有人都想介紹自己,但他們這樣做的方式不同

js
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() 實現,用於不是學生教授的人

js
pratt = new Person("Pratt");
pratt.introduceSelf(); // 'My name is Pratt.'

這種特性——當一個方法在不同類中具有相同的名稱但有不同的實現時——稱為多型性。當子類中的方法替換超類的實現時,我們說子類覆蓋了超類中的版本。

封裝

物件向其他想要使用它們的程式碼提供介面,但維護自己的內部狀態。物件的內部狀態是私有的,這意味著它只能由物件自己的方法訪問,而不能由其他物件訪問。將物件的內部狀態保持私有,並通常在其公共介面和私有內部狀態之間做出明確劃分,稱為封裝

這是一個有用的特性,因為它使程式設計師能夠更改物件的內部實現,而無需查詢和更新所有使用它的程式碼:它在該物件和系統其餘部分之間建立了一種防火牆。

例如,假設二年級及以上的學生可以學習射箭。我們可以透過公開學生的 year 屬性來實現這一點,其他程式碼可以檢查該屬性來決定學生是否可以參加課程

js
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 }
js
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 屬性上,那麼使用該建構函式建立的所有物件都透過它們的原型獲得該方法,我們不需要在建構函式中定義它。

  • 原型鏈似乎是實現繼承的自然方式。例如,如果我們有一個原型是 PersonStudent 物件,那麼它可以繼承 name 並覆蓋 introduceSelf()

但是值得理解這些特性與上面描述的“經典”OOP 概念之間的區別。我們將在這裡強調其中幾個。

首先,在基於類的 OOP 中,類和物件是兩個獨立的構造,物件總是作為類的例項建立。此外,用於定義類(類語法本身)的特性與用於例項化物件(建構函式)的特性之間存在區別。在 JavaScript 中,我們可以而且經常在沒有任何單獨類定義的情況下建立物件,無論是使用函式還是物件字面量。這使得使用物件比在經典 OOP 中輕量得多。

其次,儘管原型鏈看起來像繼承層次結構,並且在某些方面表現得像它一樣,但在其他方面它有所不同。當例項化一個子類時,會建立一個單獨的物件,該物件結合了子類中定義的屬性和層次結構中更高層定義的屬性。使用原型時,層次結構的每個級別都由一個單獨的物件表示,它們透過 __proto__ 屬性連結在一起。原型鏈的行為更像是委託,而不是繼承。委託是一種程式設計模式,其中一個物件在被要求執行任務時,可以自己執行任務或要求另一個物件(其委託者)代表它執行任務。在許多方面,委託是一種比繼承更靈活的組合物件的方式(例如,可以在執行時更改或完全替換委託者)。

儘管如此,建構函式和原型可用於在 JavaScript 中實現基於類的 OOP 模式。但是直接使用它們來實現繼承等功能是很棘手的,因此 JavaScript 提供了額外的功能,建立在原型模型之上,更直接地對映到基於類的 OOP 概念。這些額外功能是下一篇文章的主題。

總結

本文描述了基於類的面向物件程式設計的基本特性,並簡要介紹了 JavaScript 建構函式和原型與這些概念的比較。

在下一篇文章中,我們將探討 JavaScript 為支援基於類的面向物件程式設計而提供的功能。