跳轉到內容

JavaScript/基於物件程式設計

來自華夏公益教科書,開放的書籍,開放的世界



面向物件程式設計(OOP)是一種軟體設計正規化,它起源於 1960 年代,並在 1990 年代流行起來。它力求模組化、可重用性、封裝和隱藏狀態(資料)和行為(函式)、在泛化、繼承等層次結構中進行設計。

它允許元件儘可能模組化。特別是,當建立一個新的物件型別時,期望它應該能夠在不同的環境或新的程式設計專案中工作,而不會出現問題。這種方法的優點是開發時間更短,除錯更容易,因為您在重新使用已被證明有效的程式程式碼。這種“黑盒”方法意味著資料進入物件,其他資料從物件中出來,但內部發生的事情,您無需關心。

隨著時間的推移,人們開發了不同的技術來實現 OOP。最流行的是基於類和基於原型的方法。

基於類的 OOP

[編輯 | 編輯原始碼]

類是定義了一組結構相同物件的各個方面(狀態和行為)的藍圖。藍圖稱為,物件是該類的例項C 家族語言 的流行成員,尤其是 Java、C++ 和 C#,用這種基於類的方法實現 OOP。

基於原型 的 OOP

[編輯 | 編輯原始碼]

基於原型的方法中,每個物件都儲存其狀態和行為。此外,它還有一個原型(如果它位於層次結構的頂層,則為null)。這樣的原型是指向另一個更一般的物件的指標。所引用物件的所有屬性在引用物件中也可用。在基於原型的方法中,類明確不存在。

// define a constructor for the class "Class4Person"
function Class4Person () {
  this.firstname =  "";
  this.lastname  =  "";
  this.age       =  0;
  return this;
};

// define a method "set_age()" for the class "Class4Person"
Class4Person.prototype.set_age = function (pYears) {
   // set age of person
   this.age = pYears;
}


// define a method "get_older()" for the class "Class4Person"
Class4Person.prototype.get_older = function (pYears) {
   // if pYear parameter is not set, define pYears=1
   pYears = pYears || 1;
   // increment age with pYears
   this.age += pYears;
}

// define a method "get_info()" for the class "Class4Person"
Class4Person.prototype.get_info = function () {
   // create an info string about person
   return "My name is " + this.firstname + " " + this.lastname + " and I am " + this.age + " old."
}

// create instance of Class4Person
let anna = new Class4Person();

// set attributes of instance "anna"
anna.firstname = "Anna";
anna.lastname = "Miller";

// call methods of instance "anna"
anna.set_age(15);  // anna.age = 15
anna.get_older(5); // anna.age = 20
anna.get_older();  // anna.age = 21

// create instance of Class4Person
let bert = new Class4Person();

// set attributes of instance "bert"
bert.firstname = "Bert";
bert.lastname  = "Smith";

// call method for instance "bert"
bert.set_age(30);  // age of instance "bert" is now set to 30 - bert.age = 30

// create info string for instances and show in console log
console.log(anna.get_info()); // "I am Anna Miller and I am 21 years old."
console.log(bert.get_info()); // "I am Bert Smith and I am 30 years old."

OOP in JavaScript "Two jackets for one body"

[編輯 | 編輯原始碼]

JavaScript 的基石之一是根據基於原型的 OOP 的規則提供物件。物件由屬性組成,這些屬性是包含資料的鍵值對以及方法。其中一個屬性始終是原型屬性 '__proto__'。它指向“父”物件,並透過此實現了關係。


// relationships are realized by the property '__proto__'
let parent = [];
let child = {
  name:      "Joel",
  __proto__: parent,
};
console.log(Object.getPrototypeOf(child)); // Array []
┌─────────────────────┐          ┌─────────────────────┐
│        child        │    ┌──>  │       parent        │
├─────────────────────┤    |     ├─────────────────────┤
│  name: Joel         │    |     │  .... : ...         │
├─────────────────────┤    |     ├─────────────────────┤
│  __proto__: parent  │  ──┘     │  __proto__: ...     │  ──> ... ──> null
└─────────────────────┘          └─────────────────────┘

如果在任何物件上都缺少請求的屬性,JavaScript 引擎將在“父”物件、“祖父母”物件等中搜索它。這稱為原型鏈

所有這些都以相同的方式適用於使用者定義的物件和系統定義的物件,例如ArraysDate

自從 EcmaScript 2015 (ES6) 以來,語法提供了像classextends 這樣的關鍵字,這些關鍵字用於基於類的方法。即使引入了這樣的關鍵字,JavaScript 的基本原理並沒有改變:這些關鍵字以與以前相同的方式導致原型。它們是語法糖,並被編譯成傳統的原型技術。

總之,JavaScript 的語法提供了兩種在原始碼中表達面向物件特徵(如繼承)的方式:“經典”和“class”風格。儘管語法不同,但實現技術略有不同。

經典語法

[編輯 | 編輯原始碼]

從一開始,JavaScript 就使用“原型”機制來定義物件的父子關係。如果原始碼中沒有明確說明,則會自動發生這種情況。經典語法很好地展現了它。

要明確定義兩個物件的父子關係,您應該使用方法setPrototypeOf 將一個物件的原型設定為另一個專用物件。方法執行後,父物件的所有屬性(包括函式)都將為子物件所知。

<!DOCTYPE html>
<html>
<head>
  <script>
    function go() {
      "use strict";

      const adult = {
        familyName: "McAlister",
        showFamily: function() {return "The family name is: " + this.familyName;}
      };
      const child = {
        firstName: "Joel", 
        kindergarten: "House of Dwars"
      };

      // 'familyName' and 'showFamily()' are undefined here!
      alert(child.firstName + " " + child.familyName);

      // set the intended prototype chain
      Object.setPrototypeOf(child, adult);
      // or:  child.__proto__ = adult;

      alert(child.firstName + " " + child.familyName);
      alert(child.showFamily());
    }
    </script>
  </head>

  <body id="body">
    <button onclick="go()">Run the demo</button>
  </body>
</html>

“adult”物件包含“familyName”和一個函式“showFamily”。在第一步中,它們在“child”物件中是未知的。setPrototypeOf 執行後,它們將變為已知,因為“child”的原型不再指向預設的“Object”,而是指向“adult”。

下一個指令碼演示了原型鏈。它從使用者定義的變數myArraytheObject 開始。myArray 是一個包含三個元素的陣列。第 6 行的賦值操作將theObject 設定為同一個陣列。迴圈顯示theObject 的原型,並將原型鏈的下一級分配給它。當到達層次結構的頂層時,迴圈結束。在本例中,原型是null

  function go() {
    "use strict";

    // define an array with three elements
    const myArray = [0, 1, 2];
    let theObject = myArray;

    do {
      // show the object's prototype
      console.log(Object.getPrototypeOf(theObject));  // Array[], Object{...}, null
      // or: console.log(theObject.__proto__);

      // switch to the next higher level
      theObject = Object.getPrototypeOf(theObject);
    } while (theObject);
  }

如您所知,屬性是鍵值對。因此,可以直接使用“原型”屬性的值來識別和操作原型。有趣的是,鍵的名稱不是“prototype”,而是“__proto__”。這在第 11 行中顯示。但是,我們建議您忽略這種技術,而是使用用於原型操作的 API 方法,例如Object.getPrototypeOfObject.setPrototypeOfObject.create

'class' 語法

[編輯 | 編輯原始碼]

該指令碼定義了兩個類AdultChild,它們具有一些內部屬性,其中一個屬性是一個方法。關鍵字extends 按層次結構組合這兩個類。之後,在第 21 行,使用關鍵字new 建立了一個例項。

<!DOCTYPE html>
<html>
<head>
  <script>
  function go() {
    "use strict";

    class Adult {
      constructor(familyName) {
        this.familyName = familyName;
      }
      showFamily() {return "The family name is: " + this.familyName;}
    }
    class Child extends Adult {
      constructor(firstName, familyName, kindergarten) {
        super(familyName);
        this.firstName = firstName;
        this.kindergarten = kindergarten;
      }
    }

    const joel = new Child("Joel", "McAlister", "House of Dwars");
    alert(joel.firstName + " " + joel.familyName);
    alert(joel.showFamily());
  }
  </script>
</head>

<body id="body">
  <button onclick="go()">Run the demo</button>
</body>
</html>

屬性familyName 和方法showFamilyAdult 類中定義。但它們在Child 類中也是已知的。

請再次注意,JavaScript 中這種基於類的繼承是在基於原型的經典方法之上實現的。

華夏公益教科書