深入了解物件模型

  • 版本網址代稱: Web/JavaScript/Guide/Details_of_the_Object_Model
  • 版本標題: Details of the object model
  • 版本 ID: 423315
  • 建立日期:
  • 建立者: steely.wing
  • Is current revision?
  • 回應

版本內容

JavaScript 是一種基於原型的物件導向語言,而不是基於類的。正是由於這個根本的區別,使它在如何創建物件的層級結構,以及如何繼承屬性和它的值上,不是很清晰。本節將試著闡明這個問題。

本節假設您已經有點 JavaScript 的基礎,並且用 JavaScript 的函數創建過簡單的物件。

基於類 與 基於原型 語言的比較

基於類的物件導向語言,比如 Java 和 C++,是構建在兩個不同實體的概念之上的:類和實例。

  • 類(Class):定義了所有用於具有某一組特徵物件的屬性(可以將 Java 中的方法和欄位,以及 C++ 中的成員視作屬性)。類是抽象的事物,而不是其所描述的全部物件中的特定成員。例如 Employee 類可以用來表示所有僱員的集合。
  • 實例(Instance):則是類產生的實體,或者說,是它的一個成員。例如, Victoria 可以是 Employee 類的一個實例,表示一個特定的僱員個體。實例具有其父類完全一致的屬性(不多也不少)。

基於原型的語言,比如 JavaScript 它並不存在這種區別:它只有物件。基於原型的語言具有所謂原型物件(Prototypical Object)的概念。新物件在初始化時以原型物件為範本獲得屬性。任何物件都可以指定其自身的屬性,在創建時或運行時都可以。而且,任何物件都可以關聯為另一個物件的原型(Prototype),從而允許後者共用前者的屬性。

定義一個類

在基於類的語言中,類被定義在分開的類定義(Class Definition)。在類定義中,允許定義特殊的方法,稱為建構函數(Constructor),用以創建該類的實例。建構函數可以指定實例屬性的初始值,以及它的初始化處理。使用 new 操作符和建構函數來創建類的實例。

JavaScript 也遵循類似的模型,但卻沒有類定義。JavaScript 使用建構函數來定義物件的屬性及初始值,所有的 JavaScript 函數都可以作為建構函數。使用 new 操作符來建立實例。

子類和繼承

基於類的語言是通過類定義來構建類的層級結構的。在類定義中,可以指定新的類是一個現存的類的子類。子類將繼承超類的全部屬性,並可以添加新的屬性或者修改繼承的屬性。例如,假設 Employee 類只有 namedept 屬性,而 ManagerEmployee 的子類並添加了 reports 屬性。這時,Manager 類的實例將具有三個屬性:name,dept 和 reports

JavaScript 的繼承通過關聯另一個有構建函數的原型物件來實現,這樣,您可以創建完全一樣的 EmployeeManager 範例,不過使用的術語略有不同。首先,定義 Employee 構建函數,指定 namedept 屬性。然後,定義 Manager 構造器函數,指定 reports 屬性。最後,將一個新的 Employee 物件賦值給 Manager 構造器函數的 prototype 屬性。這樣,當創建一個新的 Manager 物件時,它將從 Employee 物件中繼承 name and dept 屬性。

添加和移除屬性

在 基於類的語言中,通常在編譯時創建類,然後在編譯時或者運行時產生實體類的實例。一旦定義了類,無法改變類的屬性的數目或者類型。然而,在 JavaScript 中,允許運行時添加或者移除任何物件的屬性。如果在其它物件的原型物件中添加屬性,則以該物件作為原型的所有其它物件也將獲得該屬性。

區別摘要

下面的表格摘要給出了上述區別。本節的後續部分將描述有關使用 JavaScript 構造器和原型創建物件層級結構的詳細資訊,並將其與在 Java 中的做法加以對比。

表 8.1 基於類(Java)和基於原型(JavaScript)的物件系統的比較
基於類的(Java) 基於原型的(JavaScript)
類和實例是不同的事物。 所有物件均為實例。
通過類定義來定義類;通過構造器方法來產生實體類。 通過構造器函數來定義和創建一組物件。
通過 new 操作符創建單個對象。 相同。
通過類定義來定義現存類的子類,從而構建物件的層級結構。 通過將一個物件作為原型指定關聯於構造器函數來構建物件的層級結構。
遵循類鏈繼承屬性。 遵循原型鏈繼承屬性。
類定義指定類的所有實例的所有屬性。無法在運行時添加屬性。 構造器函數或原型指定初始的屬性集。允許動態地向單個的物件或者整個物件集中添加屬性,或者從中移除屬性。

雇員示例

本節的餘下部分將使用如下圖所示的雇員層級結構。

圖例 8.1:一個簡單的物件層級

該示例中使用以下物件:

  • Employee 具有 name 屬性(其值預設為空的字串)和 dept 屬性(其值預設為 "general")。
  • Manager 基於 Employee。它添加了 reports 屬性(其值預設為空的陣列,意在以 Employee 物件的陣列作為它的值)。
  • WorkerBee 同樣基於 Employee。它添加了 projects 屬性(其值預設為空的陣列,意在以字串陣列作為它的值)。
  • SalesPerson 基於 WorkerBee。它添加了 quota 屬性(其值預設為 100)。它還重載了 dept 屬性值為 "sales",表明所有的銷售人員都屬於同一部門。
  • Engineer 基於 WorkerBee。它添加了 machine 屬性(其值預設為空的字串)同時重載了 dept 屬性值為 "engineering"。

創建層級結構

有幾種不同的方式,可以用於定義適當的構造器函數,藉以實現雇員的層級結構。如何選擇很大程度上取決於您希望在您的應用程式中能做到什麼。

本節展現了如何使用非常簡單的(同時也是相當不靈活的)定義,使得繼承得以實現。在這些定義中,無法在創建物件時指定屬性的值。新創建的物件僅僅獲得了預設值,當然允許隨後加以修改。圖例 8.2 展現了這些簡單的定義形成的層級結構。

在 真實的應用程式中,您很可能想定義允許在創建物件時給出屬性值的構造器。(參見 {{ web.link("#more_flexible_constructors", "更靈活的構造器") }} 獲得進一步的資訊)。對於現在而言,這些簡單的定義示範了繼承是如何發生的。

figure8.2.png
圖例 8.2:Employee 物件定義

以下 Employee 的 Java 和 JavaScript 的定義相類似。唯一的不同是在 Java 中需要指定每個屬性的類型,而在 JavaScript 中則不必指定,同時 Java 的類必須創建一個顯式的構造器方法。

JavaScript Java
function Employee () {
  this.name = "";
  this.dept = "general";
}
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this.name = "";
      this.dept = "general";
   }
}

ManagerWorkerBee 的定義顯示了在如何指定繼承鏈中上一層物件方面的不同點。在 JavaScript 中,需要為構造器函數的 prototype 屬性添加一個原型實例作為它的屬性值。您可以在定義了構造器之後的任何時間添加這一屬性。而在 Java 中,則需要在類定義中指定超類,且不能在類定義之外改變超類。

JavaScript Java
function Manager () {
  this.reports = [];
}
Manager.prototype = new Employee;

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;
public class Manager extends Employee {
   public Employee[] reports;
   public Manager () {
      this.reports = new Employee[0];
   }
}

public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this.projects = new String[0];
   }
}

EngineerSalesPerson 的定義創建了派生自 WorkerBee 進而派生自 Employee 的物件。這些類型的物件將具有在這個鏈之上的所有物件的屬性。同時,這些定義重載了繼承的 dept 屬性值,賦予這些屬性特定於這些物件的新的屬性值。

JavaScript Java
function SalesPerson () {
   this.dept = "sales";
   this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;

function Engineer () {
   this.dept = "engineering";
   this.machine = "";
}
Engineer.prototype = new WorkerBee;
public class SalesPerson extends WorkerBee {
   public double quota;
   public SalesPerson () {
      this.dept = "sales";
      this.quota = 100.0;
   }
}

public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      this.dept = "engineering";
      this.machine = "";
   }
}

使用這些定義,可以創建這些物件的實例。這些實例將獲得其屬性的預設值。圖例 8.3 展現了使用這些 JavaScript 定義創建新定義,並顯示了新物件中的屬性值。

{{ note('術語 實例(instance)在 基於類的語言中具有特定的技術含義。在這些語言中,實例是指類的個體成員,與類有著根本性的不同。在 JavaScript 中,“實例”並不具有這種技術含義,因為 JavaScript 中不存在類和實例之間的這種差異。然而,在談論 JavaScript 時,“實例”可以非正式地用於表示用特定的構造器函數創建的物件。所以,在這個例子中,你可以非正式地 janeEngineer 的一個實例。與之類似,儘管術語父(parent)子(child)祖先(ancestor),和後代(descendant)在 JavaScript 中並沒有正式的含義,您可以非正式地使用這些術語用於指代原型鏈中處於更高層次或者更低層次的物件。') }}

figure8.3.png
圖例 8.3:通過簡單的定義創建物件

物件的屬性

本節將討論物件如何從原型鏈中的其它物件中繼承屬性,以及在運行時添加屬性的相關細節。

繼承屬性

假設通過如下語句創建一個 mark 物件作為 WorkerBee(如 {{ web.link("#figure8.3", "圖例 8.3") }} 所示):

var mark = new WorkerBee;

當 JavaScript 發現 new 操作符,它將創建一個普通的物件,並將其作為關鍵字 this 的值傳遞給 WorkerBee 的構造器函數。該構造器函數顯式地設置 projects 屬性的值,然後隱式地將其內部的 __proto__ 屬性設置為 WorkerBee.prototype 的值(屬性的名稱前後均有兩個底線)。__proto__ 屬性決定了用於返回屬性值的原型鏈。一旦這些屬性得以設置,JavaScript 返回新創建的物件,然會設定陳述式設置變數 mark 的值為該物件。

這個過程不會顯式地為 mark 物件從原型鏈中所繼承的屬性設置值(本地值)。當請求屬性的值時,JavaScript 將首先檢查物件自身中是否設置了該屬性的值,如果有,則返回該值。如果本地值不存在,則 JavaScript 將檢查原型鏈(通過 __proto__ 屬性)。如果原型鏈中的某個物件具有該屬性的值,則返回這個值。如果沒有找到該屬性,JavaScript 則認為物件中不存在該屬性。這樣,mark 物件中將具有如下的屬性和對應的值:

mark.name = "";
mark.dept = "general";
mark.projects = [];

mark 對象從 mark.__proto__ 中保存的原型物件中繼承了 namedept 屬性的值。並由 WorkerBee 構造器函數為 projects 屬性設置了本地值。 這就是 JavaScript 中的屬性和屬性值的繼承。這個過程的一些微妙之處將在 {{ linkToFragment("再談屬性繼承") }} 中進一步討論。

由於這些構造器不支援設置實例特定的值,所以,這些屬性值僅僅是泛泛地由創建自 WorkerBee 的所有物件所共用的預設值。當然,允許修改這些屬性的值。所以,您也可以為這些屬性指定特定的值,如下所示:

mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];

添加屬性

在 JavaScript 中,可以在運行時為任何物件添加屬性,而不必受限於構造器函數提供的屬性。添加特定於某個物件的屬性,只需要為該物件指定一個屬性值,如下所示:

mark.bonus = 3000;

這樣 mark 物件就有了 bonus 屬性,而其它 WorkerBee 則沒有該屬性。

如果向某個構造器函數的原型物件中添加新的屬性,則該屬性將添加到從這個原型中繼承屬性的所有物件的中。例如,可以通過如下的語句向所有雇員中添加 specialty 屬性:

Employee.prototype.specialty = "none";

一旦 JavaScript 執行該語句,則 mark 物件也將具有 specialty 屬性,其值為 "none"。下圖展現了在 Employee 原型中添加該屬性,然後在 Engineer 的原型中重載該屬性的效果。


Figure 8.4: Adding properties

更靈活的構造器

到目前為止所展現的構造器函數不允許在創建新的實例時指定屬性值。正如 Java 一樣,可以為構造器提供參數以便初始化實例的屬性值。下圖展現其中一種做法。


Figure 8.5: Specifying properties in a constructor, take 1

下面的表格中羅列了這些物件在 Java 和 JavaScript 中的定義。

JavaScript Java
function Employee (name, dept) {
  this.name = name || "";
  this.dept = dept || "general";
}
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this("", "general");
   }
   public Employee (String name) {
      this(name, "general");
   }
   public Employee (String name, String dept) {
      this.name = name;
      this.dept = dept;
   }
}
function WorkerBee (projs) {
  this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this(new String[0]);
   }
   public WorkerBee (String[] projs) {
      projects = projs;
   }
}

 
function Engineer (mach) {
   this.dept = "engineering";
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}

這些 JavaScript 定義使用了設置預設值的一種特殊慣用法:

this.name = name || "";

JavaScript 的邏輯 OR 操作符(||)將求解它的第一個參數。如果該參數的值可以轉換為真,則操作符返回該值。否則,操作符返回第二個參數的值。因此,這行代碼首先檢查 name 是否具有一個對 name 屬性有用的值。如果有,則設置其為 this.name 的值。否則,設置 this.name 的值為空的字串。為求簡潔,本章將使用這一慣用法,儘管咋一看它有些費解。

{{ note('如果調用構造器函數時,指定了可以轉換為 false 的參數(比如 0 (零)和空字串("")),結果可能出乎調用者意料。此時,將使用預設值(譯者注:而不是指定的參數值 0 和 "")。') }}

基於這些定義,當創建物件的實例時,可以為本地定義的屬性指定值。正如 {{ web.link("#figure8.5", "圖例 8.5") }} 所示一樣,您可以通過如下語句創建新的 Engineer

var jane = new Engineer("belau");

此時,Jane 的屬性如下所示:

jane.name == "";
jane.dept == "engineering";
jane.projects == [];
jane.machine == "belau"

基於上述定義,無法為諸如 name 這樣的繼承屬性指定初始值。在 JavaScript 中,如果想為繼承的屬性指定初始值,構造器函數中需要更多的代碼。

到目前為止,構造器函數已經能夠創建一個普通物件,然後為新物件指定本地的屬性和屬性值。您還可以通過直接調用原型鏈上的更高層次物件的構造器函數,讓構造器添加更多的屬性。下面的圖例展現這種新定義。


Figure 8.6 Specifying properties in a constructor, take 2

讓我們仔細看看這些定義的其中之一。以下是 Engineer 構造器的定義:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}

假設您創建了一個新的 Engineer 物件,如下所示:

var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");

JavaScript 遵循以下步驟:

  1. new 操作符創建了一個新的普通物件,並將其 __proto__ 屬性設置為 Engineer.prototype
  2. new 操作符將該新對象作為 this 關鍵字的值傳遞給 Engineer 構造器。
  3. 構造器為該新物件創建了一個名為 base 的新屬性,並將 WorkerBee 的構造器指定為 base 屬性的值。這使得 WorkerBee 構造器成為 Engineer 物件的一個方法。base 屬性的名字沒有特殊性。可以使用任何合法的屬性名稱;base 僅僅是為了貼近它的用意。
  4. 構造器調用 base 方法,將傳遞給該構造器的參數中的兩個,作為參數傳遞給 base 方法,同時還傳遞一個字串參數  "engineering"。顯式地在構造器中使用 "engineering" 表明所有 Engineer 物件繼承的 dept 屬性具有相同的值,且該值重載了繼承自 Employee 的值。

  5. 因為 baseEngineer 的一個方法,在調用 base 時,JavaScript 將在步驟 1 中創建的對象綁定給 this 關鍵字。這樣,WorkerBee 函數接著將 "Doe, Jane""engineering" 參數傳遞給 Employee 構造器函數。當從 Employee 構造器函數返回時,WorkerBee 函數用剩下的參數設置 projects 屬性。
  6. 當從 base 方法返回時,Engineer 構造器將物件的 machine 屬性初始化為 "belau"
  7. 當從構造器返回時,JavaScript 將新物件賦值給 jane 變數。

您可以認為,在 Engineer 的構造器中調用 WorkerBee 的構造器,也就為 Engineer 物件設置好了適當繼承。事實並非如此。調用 WorkerBee 構造器確保了Engineer 物件以所有被調用的構造器中所指定的屬性作為起步。但是,如果之後在 Employee 或者 WorkerBee 原型中添加了屬性,那些屬性不會被 Engineer 物件繼承。例如,假設如下語句:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

物件 jane 不會繼承 specialty 屬性。必需顯式地設置原型才能確保動態的技能。假設修改為如下的語句:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";

現在 jane 物件的 specialty 屬性為 "none" 了。

繼承的另一種途徑是使用call() / apply() 方法。下面的方式都是等價的:

function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
function Engineer (name, projs, mach) {
  WorkerBee.call(this, name, "engineering", projs);
  this.machine = mach || "";
}

使用 javascript 的 call() 方法相對明瞭一些,因為無需 base 方法了。

再談屬性的繼承

前面的小節中描述了 JavaScript 構造器和原型如何提供層級結構和繼承的實現。本節中將討論之前未曾明確的一些細微之處。

本地的值和繼承的值

正如本章前面所述,在訪問一個物件的屬性時,JavaScript 將按照如下的步驟處理:

  1. 檢查是否存在本地的值。如果存在,返回該值。
  2. 如果本地值不存在,檢查原型鏈(通過 __proto__ 屬性)。
  3. 如果原型鏈中的某個物件具有指定屬性的值,則返回該值。
  4. 如果這樣的屬性不存在,則物件沒有該屬性。

以上步驟的結果依賴於您是如何定義的。最早的例子中具有如下定義:

function Employee () {
  this.name = "";
  this.dept = "general";
}

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

基於這些定義,假定通過如下的語句創建 WorkerBee 的實例 amy:

var amy = new WorkerBee;

amy 物件將具有一個本地屬性,projects。namedept 屬性則不是 amy 物件本地的,而是從 amy 物件的 __proto__ 屬性獲得的。因此,amy 將具有如下的屬性值:

amy.name == "";
amy.dept == "general";
amy.projects == [];

現在,假定修改了關聯於 Employee 的原型中的 name 屬性的值:

Employee.prototype.name = "Unknown"

乍一看,您可能期望新的值會傳播給所有 Employee 的實例。然而,並非如此。

在創建 Employee 對象的 任何 實例時,該實例的 name 屬性將獲得一個本地值(空的字串)。這意味著在創建一個新的 Employee 物件作為 WorkerBee 的原型時,WorkerBee.prototypename 屬性將具有一個本地值。這樣,當 JavaScript 查找 amy 物件(WorkerBee 的實例)的 name 屬性時,JavaScript 將找到 WorkerBee.prototype 中的本地值。因此,也就不會繼續在原型鏈中向上找到 Employee.prototype 了。

如果想在運行時修改物件的屬性值並且希望該值被所有該物件的後代所繼承,不能在該物件的構造器函數中定義該屬性。而是應該將該屬性添加到該物件所關聯的原型中。例如,假設將前面的代碼作如下修改:

function Employee () {
  this.dept = "general";
}
Employee.prototype.name = "";

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

var amy = new WorkerBee;

Employee.prototype.name = "Unknown";

這時,amyname 屬性將為 "Unknown"。

正如這些例子所示,如果希望物件的屬性具有預設值,且希望在運行時修改這些預設值,應該在物件的原型中設置這些屬性,而不是在構造器函數中。

判斷實例的關係

JavaScript 的屬性查找機制首先在物件自身的屬性中查找,如果指定的屬性名稱沒有找到,將在物件的特殊屬性 __proto__ 中查找。這個過程是遞迴的;被稱為“在原型鏈中查找”。

特殊的 __proto__ 屬性是在構建物件時設置的;設置為構造器的 prototype 屬性的值。所以運算式 new Foo() 將創建一個物件,其 __proto__ == Foo.prototype。因而,修改 Foo.prototype 的屬性,將改變所有通過 new Foo() 創建的物件的屬性的查找。

每個物件都有一個 __proto__ 物件屬性(除了 Object);每個函數都有一個 prototype 物件屬性。因此,通過“原型繼承(prototype inheritance)”,物件與其它物件之間形成關係。通過比較物件的 __proto__ 屬性和函數的 prototype 屬性可以檢測物件的繼承關係。JavaScript 提供了便捷方法:instanceof 操作符可以用來將一個物件和一個函數做檢測,如果物件繼承自函數的原型,則該操作符返回真。例如:

var f = new Foo();
var isTrue = (f instanceof Foo);

作為詳細一點的例子,假定我們使用和在 {{ linkToFragment("繼承屬性") }} 中相同的一組定義。創建 Engineer 物件如下:

var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");

對於該物件,以下所有語句均為真:

chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;

基於此,可以寫出一個如下所示的 instanceOf 函數:

function instanceOf(object, constructor) {
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}
Note: 在上面的實現中,檢查物件的類型是否為 "xml" 的目的在於解決新近版本的 JavaScript 中表達 XML 物件的特異之處。如果您想瞭解其中瑣碎細節,可以參考 {{ bug(634150) }}。
instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)

但如下運算式為假:

instanceOf (chris, SalesPerson)

構造器中的全域資訊

在創建構造器時,在構造器中設置全域資訊要小心。例如,假設希望為每一個雇員分配一個唯一標識。可能會為 Employee 使用如下定義:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

基於該定義,在創建新的 Employee 時,構造器為其分配了序列中的下一個識別字。然後遞增全域的識別字計數器。因此,如果,如果隨後的語句如下,則 victoria.id 為 1 而 harry.id 為 2:

var victoria = new Employee("Pigbert, Victoria", "pubs")
var harry = new Employee("Tschopik, Harry", "sales")

乍一看似乎沒問題。但是,無論什麼目的,在每一次創建 Employee 對象時,idCounter 都將被遞增一次。如果創建本章中所描述的整個 Employee 層級結構,每次設置原型的時候,Employee 構造器都將被調用一次。假設有如下代碼:

var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;

function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;

function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;

function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;

var mac = new Engineer("Wood, Mac");

還可以進一步假設上面省略掉的定義中包含 base 屬性而且調用了原型鏈中高於它們的構造器。即便在現在這個情況下,在 mac 物件創建時,mac.id 為 5。

依賴于應用程式,計數器額外的遞增可能有問題,也可能沒問題。如果確實需要準確的計數器,則以下構造器可以作為一個可行的方案:

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   if (name)
      this.id = idCounter++;
}

在用作原型而創建新的 Employee 實例時,不會指定參數。使用這個構造器定義,如果不指定參數,構造器不會指定識別字,也不會遞增計數器。而如果想讓 Employee 分配到識別字,則必需為雇員指定姓名。在這個例子中,mac.id 將為 1。

沒有多繼承

某些物件導向語言支援多重繼承。也就是說,物件可以從無關的多個父物件中繼承屬性和屬性值。JavaScript 不支持多重繼承。

JavaScript 屬性值的繼承是在運行時通過檢索物件的原型鏈來實現的。因為物件只有一個原型與之關聯,所以 JavaScript 無法動態地從多個原型鏈中繼承。

在 JavaScript 中,可以在構造器函數中調用多個其它的構造器函數。這一點造成了多重繼承的假像。例如,考慮如下語句:

function Hobbyist (hobby) {
   this.hobby = hobby || "scuba";
}

function Engineer (name, projs, mach, hobby) {
   this.base1 = WorkerBee;
   this.base1(name, "engineering", projs);
   this.base2 = Hobbyist;
   this.base2(hobby);
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")

進一步假設使用本章前面所屬的 WorkerBee 的定義。此時 dennis 物件具有如下屬性:

dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"

dennis 確實從 Hobbyist 構造器中獲得了 hobby 屬性。但是,假設添加了一個屬性到 Hobbyist 構造器的原型:

Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]

dennis 物件不會繼承這個新屬性。

{{ PreviousNext("JavaScript/Guide/Predefined_Core_Objects", "JavaScript/Guide/Inheritance_Revisited") }}

版本來源

<p>JavaScript 是一種基於原型的物件導向語言,而不是基於類的。正是由於這個根本的區別,使它在如何創建物件的層級結構,以及如何繼承屬性和它的值上,不是很清晰。本節將試著闡明這個問題。</p>
<p>本節假設您已經有點 JavaScript 的基礎,並且用 JavaScript 的函數創建過簡單的物件。</p>
<h2 id="class-based_vs_prototype-based_languages" name="class-based_vs_prototype-based_languages">基於類 與 基於原型 語言的比較</h2>
<p>基於類的物件導向語言,比如 Java 和 C++,是構建在兩個不同實體的概念之上的:類和實例。</p>
<ul>
  <li><em>類(Class):</em>定義了所有用於具有某一組特徵物件的屬性(可以將 Java 中的方法和<span data-aligning="#src_1_17,#tran_1_15" id="tran_1_15">欄位</span>,以及 C++ 中的成員視作屬性)。類是抽象的事物,而不是其所描述的全部物件中的特定成員。例如 <code>Employee</code> 類可以用來表示所有僱員的集合。</li>
  <li><em>實例(Instance):</em>則是類產生的實體,或者說,是它的一個成員。例如, <code>Victoria</code> 可以是 <code>Employee</code> 類的一個實例,表示一個特定的僱員個體。實例具有其父類完全一致的屬性(不多也不少)。</li>
</ul>
<p>基於原型的語言,比如 JavaScript 它並不存在這種區別:它只有物件。基於原型的語言具有所謂<em>原型物件(Prototypical Object)</em>的概念。新物件在初始化時以原型物件為範本獲得屬性。任何物件都可以指定其自身的屬性,在創建時或運行時都可以。而且,任何物件都可以關聯為另一個物件的<em>原型(Prototype)</em>,從而允許後者共用前者的屬性。</p>
<h3 id=".E5.AE.9A.E7.BE.A9.E4.B8.80.E5.80.8B.E9.A1.9E">定義一個類</h3>
<p>在基於類的語言中,類被定義在分開的<em>類定義(Class Definition)</em>。在類定義中,允許定義特殊的方法,稱為<em>建構函數(Constructor)</em>,用以創建該類的實例。建構函數可以指定實例屬性的初始值,以及它的初始化處理。使用 <code>new</code> 操作符和建構函數來創建類的實例。</p>
<p>JavaScript 也遵循類似的模型,但卻沒有類定義。JavaScript 使用建構函數來定義物件的屬性及初始值,所有的 JavaScript 函數都可以作為建構函數。使用 <code>new</code> 操作符來建立實例。</p>
<h3 id=".E5.AD.90.E9.A1.9E.E5.92.8C.E7.B9.BC.E6.89.BF">子類和繼承</h3>
<p>基於類的語言是通過類定義來構建類的層級結構的。在類定義中,可以指定新的類是一個現存的類的<em>子類</em>。子類將繼承超類的全部屬性,並可以添加新的屬性或者修改繼承的屬性。例如,假設 <code>Employee</code> 類只有 <code>name</code> 和 <code>dept</code> 屬性,而 <code>Manager</code> 是 <code>Employee</code> 的子類並添加了 <code>reports</code> 屬性。這時,<code>Manager</code> 類的實例將具有三個屬性:<code>name,</code><code>dept 和</code> <code>reports</code>。</p>
<p>JavaScript 的繼承通過關聯另一個有構建函數的原型物件來實現,這樣,您可以創建完全一樣的 <code>Employee</code> — <code>Manager</code> 範例,不過使用的術語略有不同。首先,定義 <code>Employee</code> 構建函數,指定 <code>name</code> 和 <code>dept</code> 屬性。然後,定義 <code>Manager</code> 構造器函數,指定 <code>reports</code> 屬性。最後,將一個新的 <code>Employee</code> 物件賦值給 <code>Manager</code> 構造器函數的 <code>prototype</code> 屬性。這樣,當創建一個新的 <code>Manager</code> 物件時,它將從 <code>Employee</code> 物件中繼承 <code>name</code> and <code>dept</code> 屬性。</p>
<h3 id=".E6.B7.BB.E5.8A.A0.E5.92.8C.E7.A7.BB.E9.99.A4.E5.B1.AC.E6.80.A7">添加和移除屬性</h3>
<p>在 基於類的語言中,通常在編譯時創建類,然後在編譯時或者運行時產生實體類的實例。一旦定義了類,無法改變類的屬性的數目或者類型。然而,在 JavaScript 中,允許運行時添加或者移除任何物件的屬性。如果在其它物件的原型物件中添加屬性,則以該物件作為原型的所有其它物件也將獲得該屬性。</p>
<h3 id=".E5.8D.80.E5.88.A5.E6.91.98.E8.A6.81">區別摘要</h3>
<p>下面的表格摘要給出了上述區別。本節的後續部分將描述有關使用 JavaScript 構造器和原型創建物件層級結構的詳細資訊,並將其與在 Java 中的做法加以對比。</p>
<table class="fullwidth-table">
  <caption style="text-align: left;">
    表 8.1 基於類(Java)和基於原型(JavaScript)的物件系統的比較</caption>
  <thead>
    <tr>
      <th scope="col">基於類的(Java)</th>
      <th scope="col">基於原型的(JavaScript)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>類和實例是不同的事物。</td>
      <td>所有物件均為實例。</td>
    </tr>
    <tr>
      <td>通過類定義來定義類;通過構造器方法來產生實體類。</td>
      <td>通過構造器函數來定義和創建一組物件。</td>
    </tr>
    <tr>
      <td>通過 <code>new</code> 操作符創建單個對象。</td>
      <td>相同。</td>
    </tr>
    <tr>
      <td>通過類定義來定義現存類的子類,從而構建物件的層級結構。</td>
      <td>通過將一個物件作為原型指定關聯於構造器函數來構建物件的層級結構。</td>
    </tr>
    <tr>
      <td>遵循類鏈繼承屬性。</td>
      <td>遵循原型鏈繼承屬性。</td>
    </tr>
    <tr>
      <td>類定義指定類的所有實例的<em>所有</em>屬性。無法在運行時添加屬性。</td>
      <td>構造器函數或原型指定初始的屬性集。允許動態地向單個的物件或者整個物件集中添加屬性,或者從中移除屬性。</td>
    </tr>
  </tbody>
</table>
<h2 id=".E9.9B.87.E5.93.A1.E7.A4.BA.E4.BE.8B">雇員示例</h2>
<p>本節的餘下部分將使用如下圖所示的雇員層級結構。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4452/=figure8.1.png" style="width: 281px; height: 194px;" /></p>
<p><small><strong>圖例 8.1:一個簡單的物件層級</strong></small></p>
<p>該示例中使用以下物件:</p>
<ul>
  <li><code>Employee</code> 具有 <code>name</code> 屬性(其值預設為空的字串)和 <code>dept</code> 屬性(其值預設為 "general")。</li>
  <li><code>Manager</code> 基於 <code>Employee。它添加了</code> <code>reports</code> 屬性(其值預設為空的陣列,意在以 <code>Employee</code> 物件的陣列作為它的值)。</li>
  <li><code>WorkerBee</code> 同樣基於 <code>Employee</code>。它添加了 <code>projects</code> 屬性(其值預設為空的陣列,意在以字串陣列作為它的值)。</li>
  <li><code>SalesPerson</code> 基於 <code>WorkerBee</code>。它添加了 <code>quota</code> 屬性(其值預設為 100)。它還重載了 <code>dept</code> 屬性值為 "sales",表明所有的銷售人員都屬於同一部門。</li>
  <li><code>Engineer</code> 基於 <code>WorkerBee</code>。它添加了 <code>machine</code> 屬性(其值預設為空的字串)同時重載了 <code>dept</code> 屬性值為 "engineering"。</li>
</ul>
<h2 id=".E5.89.B5.E5.BB.BA.E5.B1.A4.E7.B4.9A.E7.B5.90.E6.A7.8B">創建層級結構</h2>
<p>有幾種不同的方式,可以用於定義適當的構造器函數,藉以實現雇員的層級結構。如何選擇很大程度上取決於您希望在您的應用程式中能做到什麼。</p>
<p>本節展現了如何使用非常簡單的(同時也是相當不靈活的)定義,使得繼承得以實現。在這些定義中,無法在創建物件時指定屬性的值。新創建的物件僅僅獲得了預設值,當然允許隨後加以修改。圖例 8.2 展現了這些簡單的定義形成的層級結構。</p>
<p>在 真實的應用程式中,您很可能想定義允許在創建物件時給出屬性值的構造器。(參見 {{ web.link("#more_flexible_constructors", "更靈活的構造器") }} 獲得進一步的資訊)。對於現在而言,這些簡單的定義示範了繼承是如何發生的。</p>
<p><img alt="figure8.2.png" class="internal default" src="/@api/deki/files/4390/=figure8.2.png" /><br />
  <small><strong>圖例 8.2:Employee 物件定義</strong></small></p>
<p>以下 <code>Employee</code> 的 Java 和 JavaScript 的定義相類似。唯一的不同是在 Java 中需要指定每個屬性的類型,而在 JavaScript 中則不必指定,同時 Java 的類必須創建一個顯式的構造器方法。</p>
<table class="standard-table">
  <thead>
    <tr>
      <th scope="col">JavaScript</th>
      <th scope="col">Java</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <pre class="brush: js">
function Employee () {
  this.name = "";
  this.dept = "general";
}
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this.name = "";
      this.dept = "general";
   }
}
</pre>
      </td>
    </tr>
  </tbody>
</table>
<p><code>Manager</code> 和 <code>WorkerBee</code> 的定義顯示了在如何指定繼承鏈中上一層物件方面的不同點。在 JavaScript 中,需要為構造器函數的 <code>prototype</code> 屬性添加一個原型實例作為它的屬性值。您可以在定義了構造器之後的任何時間添加這一屬性。而在 Java 中,則需要在類定義中指定超類,且不能在類定義之外改變超類。</p>
<table class="standard-table">
  <thead>
    <tr>
      <th scope="col">JavaScript</th>
      <th scope="col">Java</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <pre class="brush: js">
function Manager () {
  this.reports = [];
}
Manager.prototype = new Employee;

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class Manager extends Employee {
   public Employee[] reports;
   public Manager () {
      this.reports = new Employee[0];
   }
}

public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this.projects = new String[0];
   }
}
</pre>
      </td>
    </tr>
  </tbody>
</table>
<p><code>Engineer</code> 和 <code>SalesPerson</code> 的定義創建了派生自 <code>WorkerBee</code> 進而派生自 <code>Employee</code> 的物件。這些類型的物件將具有在這個鏈之上的所有物件的屬性。同時,這些定義重載了繼承的 <code>dept</code> 屬性值,賦予這些屬性特定於這些物件的新的屬性值。</p>
<table class="standard-table">
  <thead>
    <tr>
      <th scope="col">JavaScript</th>
      <th scope="col">Java</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <pre class="brush: js">
function SalesPerson () {
   this.dept = "sales";
   this.quota = 100;
}
SalesPerson.prototype = new WorkerBee;

function Engineer () {
   this.dept = "engineering";
   this.machine = "";
}
Engineer.prototype = new WorkerBee;
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class SalesPerson extends WorkerBee {
   public double quota;
   public SalesPerson () {
      this.dept = "sales";
      this.quota = 100.0;
   }
}

public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      this.dept = "engineering";
      this.machine = "";
   }
}
</pre>
      </td>
    </tr>
  </tbody>
</table>
<p>使用這些定義,可以創建這些物件的實例。這些實例將獲得其屬性的預設值。圖例 8.3 展現了使用這些 JavaScript 定義創建新定義,並顯示了新物件中的屬性值。</p>
<p>{{ note('術語 <em><em>實例(instance)</em></em>在 基於類的語言中具有特定的技術含義。在這些語言中,實例是指類的個體成員,與類有著根本性的不同。在 JavaScript 中,“實例”並不具有這種技術含義,因為 JavaScript 中不存在類和實例之間的這種差異。然而,在談論 JavaScript 時,“實例”可以非正式地用於表示用特定的構造器函數創建的物件。所以,在這個例子中,你可以非正式地 <code>jane</code> 是 <code>Engineer</code> 的一個實例。與之類似,儘管術語<em>父(parent)</em>,<em>子(child)</em>,<em>祖先(ancestor)</em>,和<em>後代(descendant)</em>在 JavaScript 中並沒有正式的含義,您可以非正式地使用這些術語用於指代原型鏈中處於更高層次或者更低層次的物件。') }}</p>
<p><img alt="figure8.3.png" class="internal default" id="figure8.3" src="/@api/deki/files/4403/=figure8.3.png" /><br />
  <small><strong>圖例 8.3:通過簡單的定義創建物件</strong></small></p>
<h2 id=".E7.89.A9.E4.BB.B6.E7.9A.84.E5.B1.AC.E6.80.A7">物件的屬性</h2>
<p>本節將討論物件如何從原型鏈中的其它物件中繼承屬性,以及在運行時添加屬性的相關細節。</p>
<h3 id=".E7.B9.BC.E6.89.BF.E5.B1.AC.E6.80.A7">繼承屬性</h3>
<p>假設通過如下語句創建一個 <code>mark</code> 物件作為 <code>WorkerBee</code>(如 {{ web.link("#figure8.3", "圖例 8.3") }} 所示):</p>
<pre class="brush: js">
var mark = new WorkerBee;
</pre>
<p>當 JavaScript 發現 <code>new</code> 操作符,它將創建一個普通的物件,並將其作為關鍵字 <code>this</code> 的值傳遞給 <code>WorkerBee</code> 的構造器函數。該構造器函數顯式地設置 <code>projects</code> 屬性的值,然後隱式地將其內部的 <code>__proto__</code> 屬性設置為 <code>WorkerBee.prototype</code> 的值(屬性的名稱前後均有兩個底線)。<code>__proto__</code> 屬性決定了用於返回屬性值的原型鏈。一旦這些屬性得以設置,JavaScript 返回新創建的物件,然會設定陳述式設置變數 <code>mark</code> 的值為該物件。</p>
<p>這個過程不會顯式地為 <code>mark</code> 物件從原型鏈中所繼承的屬性設置值(本地值)。當請求屬性的值時,JavaScript 將首先檢查物件自身中是否設置了該屬性的值,如果有,則返回該值。如果本地值不存在,則 JavaScript 將檢查原型鏈(通過 <code>__proto__</code> 屬性)。如果原型鏈中的某個物件具有該屬性的值,則返回這個值。如果沒有找到該屬性,JavaScript 則認為物件中不存在該屬性。這樣,<code>mark</code> 物件中將具有如下的屬性和對應的值:</p>
<pre class="brush: js">
mark.name = "";
mark.dept = "general";
mark.projects = [];
</pre>
<p><code>mark</code> 對象從 <code>mark.__proto__</code> 中保存的原型物件中繼承了 <code>name</code> 和 <code>dept</code> 屬性的值。並由 <code>WorkerBee</code> 構造器函數為 <code>projects</code> 屬性設置了本地值。 這就是 JavaScript 中的屬性和屬性值的繼承。這個過程的一些微妙之處將在 {{ linkToFragment("再談屬性繼承") }} 中進一步討論。</p>
<p>由於這些構造器不支援設置實例特定的值,所以,這些屬性值僅僅是泛泛地由創建自 <code>WorkerBee</code> 的所有物件所共用的預設值。當然,允許修改這些屬性的值。所以,您也可以為這些屬性指定特定的值,如下所示:</p>
<pre class="brush: js">
mark.name = "Doe, Mark";
mark.dept = "admin";
mark.projects = ["navigator"];</pre>
<h3 id=".E6.B7.BB.E5.8A.A0.E5.B1.AC.E6.80.A7">添加屬性</h3>
<p>在 JavaScript 中,可以在運行時為任何物件添加屬性,而不必受限於構造器函數提供的屬性。添加特定於某個物件的屬性,只需要為該物件指定一個屬性值,如下所示:</p>
<pre class="brush: js">
mark.bonus = 3000;
</pre>
<p>這樣 <code>mark</code> 物件就有了 <code>bonus</code> 屬性,而其它 <code>WorkerBee</code> 則沒有該屬性。</p>
<p>如果向某個構造器函數的原型物件中添加新的屬性,則該屬性將添加到從這個原型中繼承屬性的所有物件的中。例如,可以通過如下的語句向所有雇員中添加 <code>specialty</code> 屬性:</p>
<pre class="brush: js">
Employee.prototype.specialty = "none";
</pre>
<p>一旦 JavaScript 執行該語句,則 <code>mark</code> 物件也將具有 <code>specialty</code> 屬性,其值為 <code>"none"</code>。下圖展現了在 <code>Employee</code> 原型中添加該屬性,然後在 <code>Engineer</code> 的原型中重載該屬性的效果。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4422/=figure8.4.png" style="width: 833px; height: 519px;" /><br />
  <small><strong>Figure 8.4: Adding properties</strong></small></p>
<h2 id="more_flexible_constructors" name="more_flexible_constructors">更靈活的構造器</h2>
<p>到目前為止所展現的構造器函數不允許在創建新的實例時指定屬性值。正如 Java 一樣,可以為構造器提供參數以便初始化實例的屬性值。下圖展現其中一種做法。</p>
<p><img alt="" class="internal" id="figure8.5" src="/@api/deki/files/4423/=figure8.5.png" style="width: 1012px; height: 481px;" /><br />
  <small><strong>Figure 8.5: Specifying properties in a constructor, take 1</strong></small></p>
<p>下面的表格中羅列了這些物件在 Java 和 JavaScript 中的定義。</p>
<table class="standard-table">
  <thead>
    <tr>
      <th scope="col">JavaScript</th>
      <th scope="col">Java</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>
        <pre class="brush: js">
function Employee (name, dept) {
  this.name = name || "";
  this.dept = dept || "general";
}
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class Employee {
   public String name;
   public String dept;
   public Employee () {
      this("", "general");
   }
   public Employee (String name) {
      this(name, "general");
   }
   public Employee (String name, String dept) {
      this.name = name;
      this.dept = dept;
   }
}
</pre>
      </td>
    </tr>
    <tr>
      <td>
        <pre class="brush: js">
function WorkerBee (projs) {
  this.projects = projs || [];
}
WorkerBee.prototype = new Employee;
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class WorkerBee extends Employee {
   public String[] projects;
   public WorkerBee () {
      this(new String[0]);
   }
   public WorkerBee (String[] projs) {
      projects = projs;
   }
}

</pre>
      </td>
    </tr>
    <tr>
      <td>
        <pre class="brush: js">
 
function Engineer (mach) {
   this.dept = "engineering";
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
</pre>
      </td>
      <td>
        <pre class="brush: java">
public class Engineer extends WorkerBee {
   public String machine;
   public Engineer () {
      dept = "engineering";
      machine = "";
   }
   public Engineer (String mach) {
      dept = "engineering";
      machine = mach;
   }
}
</pre>
      </td>
    </tr>
  </tbody>
</table>
<p>這些 JavaScript 定義使用了設置預設值的一種特殊慣用法:</p>
<pre class="brush: js">
this.name = name || "";
</pre>
<p>JavaScript 的邏輯 OR 操作符(<code>||)</code>將求解它的第一個參數。如果該參數的值可以轉換為真,則操作符返回該值。否則,操作符返回第二個參數的值。因此,這行代碼首先檢查 <code>name</code> 是否具有一個對 <code>name</code> 屬性有用的值。如果有,則設置其為 <code>this.name</code> 的值。否則,設置 <code>this.name</code> 的值為空的字串。為求簡潔,本章將使用這一慣用法,儘管咋一看它有些費解。</p>
<p>{{ note('如果調用構造器函數時,指定了可以轉換為 <code><code>false</code></code> 的參數(比如 <code>0</code> (零)和空字串<code><code>("")),結果可能出乎調用者意料。此時,將使用預設值(譯者注:而不是指定的參數值 0 和 "")。</code></code>') }}</p>
<p>基於這些定義,當創建物件的實例時,可以為本地定義的屬性指定值。正如 {{ web.link("#figure8.5", "圖例 8.5") }} 所示一樣,您可以通過如下語句創建新的 <code>Engineer</code>:</p>
<pre class="brush: js">
var jane = new Engineer("belau");
</pre>
<p>此時,<code>Jane</code> 的屬性如下所示:</p>
<pre class="brush: js">
jane.name == "";
jane.dept == "engineering";
jane.projects == [];
jane.machine == "belau"
</pre>
<p>基於上述定義,無法為諸如 <code>name</code> 這樣的繼承屬性指定初始值。在 JavaScript 中,如果想為繼承的屬性指定初始值,構造器函數中需要更多的代碼。</p>
<p>到目前為止,構造器函數已經能夠創建一個普通物件,然後為新物件指定本地的屬性和屬性值。您還可以通過直接調用原型鏈上的更高層次物件的構造器函數,讓構造器添加更多的屬性。下面的圖例展現這種新定義。</p>
<p><img alt="" class="internal" src="/@api/deki/files/4430/=figure8.6.png" style="width: 1063px; height: 534px;" /><br />
  <small><strong>Figure 8.6 Specifying properties in a constructor, take 2</strong></small></p>
<p>讓我們仔細看看這些定義的其中之一。以下是 <code>Engineer</code> 構造器的定義:</p>
<pre class="brush: js">
function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
</pre>
<p>假設您創建了一個新的 <code>Engineer</code> 物件,如下所示:</p>
<pre class="brush: js">
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
</pre>
<p>JavaScript 遵循以下步驟:</p>
<ol>
  <li><code>new</code> 操作符創建了一個新的普通物件,並將其 <code>__proto__</code> 屬性設置為 <code>Engineer.prototype</code>。</li>
  <li><code>new</code> 操作符將該新對象作為 <code>this</code> 關鍵字的值傳遞給 <code>Engineer</code> 構造器。</li>
  <li>構造器為該新物件創建了一個名為 <code>base</code> 的新屬性,並將 <code>WorkerBee</code> 的構造器指定為 <code>base</code> 屬性的值。這使得 <code>WorkerBee</code> 構造器成為 <code>Engineer</code> 物件的一個方法。<code>base</code> 屬性的名字沒有特殊性。可以使用任何合法的屬性名稱;<code>base</code> 僅僅是為了貼近它的用意。</li>
  <li>
    <p>構造器調用 <code>base</code> 方法,將傳遞給該構造器的參數中的兩個,作為參數傳遞給 <code>base</code> 方法,同時還傳遞一個字串參數&nbsp; <code>"engineering"。顯式地在構造器中使用</code> <code>"engineering"</code> 表明所有 <code>Engineer</code> 物件繼承的 <code>dept</code> 屬性具有相同的值,且該值重載了繼承自 <code>Employee</code> 的值。</p>
  </li>
  <li>因為 <code>base</code> 是 <code>Engineer</code> 的一個方法,在調用 <code>base</code> 時,JavaScript 將在步驟 1 中創建的對象綁定給 <code>this</code> 關鍵字。這樣,<code>WorkerBee</code> 函數接著將 <code>"Doe, Jane"</code> 和 <code>"engineering"</code> 參數傳遞給 <code>Employee</code> 構造器函數。當從 <code>Employee</code> 構造器函數返回時,<code>WorkerBee</code> 函數用剩下的參數設置 <code>projects</code> 屬性。</li>
  <li>當從 <code>base</code> 方法返回時,<code>Engineer</code> 構造器將物件的 <code>machine</code> 屬性初始化為 <code>"belau"</code>。</li>
  <li>當從構造器返回時,JavaScript 將新物件賦值給 <code>jane</code> 變數。</li>
</ol>
<p>您可以認為,在 <code>Engineer</code> 的構造器中調用 <code>WorkerBee</code> 的構造器,也就為 <code>Engineer</code> 物件設置好了適當繼承。事實並非如此。調用 <code>WorkerBee</code> 構造器確保了<code>Engineer</code> 物件以所有被調用的構造器中所指定的屬性作為起步。但是,如果之後在 <code>Employee</code> 或者 <code>WorkerBee</code> 原型中添加了屬性,那些屬性不會被 <code>Engineer</code> 物件繼承。例如,假設如下語句:</p>
<pre class="brush: js">
function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
</pre>
<p>物件 <code>jane</code> 不會繼承 <code>specialty</code> 屬性。必需顯式地設置原型才能確保動態的技能。假設修改為如下的語句:</p>
<pre class="brush: js">
function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;
var jane = new Engineer("Doe, Jane", ["navigator", "javascript"], "belau");
Employee.prototype.specialty = "none";
</pre>
<p>現在 <code>jane</code> 物件的 <code>specialty</code> 屬性為 "none" 了。</p>
<p>繼承的另一種途徑是使用<a href="/en-US/docs/JavaScript/Reference/Global_Objects/Function/call" title="en-US/docs/JavaScript/Reference/Global Objects/Function/call"><code>call()</code></a> / <a href="/en-US/docs/JavaScript/Reference/Global_Objects/Function/apply" title="en-US/docs/JavaScript/Reference/Global Objects/Function/apply"><code>apply()</code></a> 方法。下面的方式都是等價的:</p>
<table>
  <tbody>
    <tr>
      <td>
        <pre class="brush: js">
function Engineer (name, projs, mach) {
  this.base = WorkerBee;
  this.base(name, "engineering", projs);
  this.machine = mach || "";
}
</pre>
      </td>
      <td>
        <pre class="brush: js">
function Engineer (name, projs, mach) {
  WorkerBee.call(this, name, "engineering", projs);
  this.machine = mach || "";
}
</pre>
      </td>
    </tr>
  </tbody>
</table>
<p>使用 javascript 的 <code>call()</code> 方法相對明瞭一些,因為無需 <code>base</code> 方法了。</p>
<h2 id=".E5.86.8D.E8.AB.87.E5.B1.AC.E6.80.A7.E7.9A.84.E7.B9.BC.E6.89.BF">再談屬性的繼承</h2>
<p>前面的小節中描述了 JavaScript 構造器和原型如何提供層級結構和繼承的實現。本節中將討論之前未曾明確的一些細微之處。</p>
<h3 id=".E6.9C.AC.E5.9C.B0.E7.9A.84.E5.80.BC.E5.92.8C.E7.B9.BC.E6.89.BF.E7.9A.84.E5.80.BC">本地的值和繼承的值</h3>
<p>正如本章前面所述,在訪問一個物件的屬性時,JavaScript 將按照如下的步驟處理:</p>
<ol>
  <li>檢查是否存在本地的值。如果存在,返回該值。</li>
  <li>如果本地值不存在,檢查原型鏈(通過 <code>__proto__</code> 屬性)。</li>
  <li>如果原型鏈中的某個物件具有指定屬性的值,則返回該值。</li>
  <li>如果這樣的屬性不存在,則物件沒有該屬性。</li>
</ol>
<p>以上步驟的結果依賴於您是如何定義的。最早的例子中具有如下定義:</p>
<pre class="brush: js">
function Employee () {
  this.name = "";
  this.dept = "general";
}

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;
</pre>
<p>基於這些定義,假定通過如下的語句創建 <code>WorkerBee</code> 的實例 <code>amy:</code></p>
<pre class="brush: js">
var amy = new WorkerBee;
</pre>
<p>則 <code>amy</code> 物件將具有一個本地屬性,<code>projects。</code><code>name</code> 和 <code>dept</code> 屬性則不是 <code>amy</code> 物件本地的,而是從 <code>amy</code> 物件的 <code>__proto__</code> 屬性獲得的。因此,<code>amy</code> 將具有如下的屬性值:</p>
<pre class="brush: js">
amy.name == "";
amy.dept == "general";
amy.projects == [];
</pre>
<p>現在,假定修改了關聯於 <code>Employee</code> 的原型中的 <code>name</code> 屬性的值:</p>
<pre class="brush: js">
Employee.prototype.name = "Unknown"
</pre>
<p>乍一看,您可能期望新的值會傳播給所有 <code>Employee</code> 的實例。然而,並非如此。</p>
<p>在創建 <code>Employee</code> 對象的 <span style="font-style: italic;">任何</span> 實例時,該實例的 <code>name</code> 屬性將獲得一個本地值(空的字串)。這意味著在創建一個新的 <code>Employee</code> 物件作為 <code>WorkerBee</code> 的原型時,<code>WorkerBee.prototype</code> 的 <code>name</code> 屬性將具有一個本地值。這樣,當 JavaScript 查找 <code>amy</code> 物件(<code>WorkerBee</code> 的實例)的 <code>name</code> 屬性時,JavaScript 將找到 <code>WorkerBee.prototype</code> 中的本地值。因此,也就不會繼續在原型鏈中向上找到 <code>Employee.prototype</code> 了。</p>
<p>如果想在運行時修改物件的屬性值並且希望該值被所有該物件的後代所繼承,不能在該物件的構造器函數中定義該屬性。而是應該將該屬性添加到該物件所關聯的原型中。例如,假設將前面的代碼作如下修改:</p>
<pre class="brush: js">
function Employee () {
  this.dept = "general";
}
Employee.prototype.name = "";

function WorkerBee () {
  this.projects = [];
}
WorkerBee.prototype = new Employee;

var amy = new WorkerBee;

Employee.prototype.name = "Unknown";
</pre>
<p>這時,<code>amy</code> 的 <code>name</code> 屬性將為 "Unknown"。</p>
<p>正如這些例子所示,如果希望物件的屬性具有預設值,且希望在運行時修改這些預設值,應該在物件的原型中設置這些屬性,而不是在構造器函數中。</p>
<h3 id=".E5.88.A4.E6.96.B7.E5.AF.A6.E4.BE.8B.E7.9A.84.E9.97.9C.E4.BF.82">判斷實例的關係</h3>
<p>JavaScript 的屬性查找機制首先在物件自身的屬性中查找,如果指定的屬性名稱沒有找到,將在物件的特殊屬性 <code>__proto__</code> 中查找。這個過程是遞迴的;被稱為“在原型鏈中查找”。</p>
<p>特殊的 <code>__proto__</code> 屬性是在構建物件時設置的;設置為構造器的 <code>prototype</code> 屬性的值。所以運算式 <code>new Foo()</code> 將創建一個物件,其 <code>__proto__ == <code class="moz-txt-verticalline">Foo.prototype</code></code>。因而,修改 <code class="moz-txt-verticalline">Foo.prototype</code> 的屬性,將改變所有通過 <code>new Foo()</code> 創建的物件的屬性的查找。</p>
<p>每個物件都有一個 <code>__proto__</code> 物件屬性(除了 <code>Object);每個函數都有一個</code> <code>prototype</code> 物件屬性。因此,通過“原型繼承(prototype inheritance)”,物件與其它物件之間形成關係。通過比較物件的 <code>__proto__</code> 屬性和函數的 <code>prototype</code> 屬性可以檢測物件的繼承關係。JavaScript 提供了便捷方法:<code>instanceof</code> 操作符可以用來將一個物件和一個函數做檢測,如果物件繼承自函數的原型,則該操作符返回真。例如:</p>
<pre class="brush: js">
var f = new Foo();
var isTrue = (f instanceof Foo);</pre>
<p>作為詳細一點的例子,假定我們使用和在 {{ linkToFragment("繼承屬性") }} 中相同的一組定義。創建 <code>Engineer</code> 物件如下:</p>
<pre class="brush: js">
var chris = new Engineer("Pigman, Chris", ["jsd"], "fiji");
</pre>
<p>對於該物件,以下所有語句均為真:</p>
<pre class="brush: js">
chris.__proto__ == Engineer.prototype;
chris.__proto__.__proto__ == WorkerBee.prototype;
chris.__proto__.__proto__.__proto__ == Employee.prototype;
chris.__proto__.__proto__.__proto__.__proto__ == Object.prototype;
chris.__proto__.__proto__.__proto__.__proto__.__proto__ == null;
</pre>
<p>基於此,可以寫出一個如下所示的 <code>instanceOf</code> 函數:</p>
<pre class="brush: js">
function instanceOf(object, constructor) {
   while (object != null) {
      if (object == constructor.prototype)
         return true;
      if (typeof object == 'xml') {
        return constructor.prototype == XML.prototype;
      }
      object = object.__proto__;
   }
   return false;
}
</pre>
<div class="note">
  <strong>Note:</strong> 在上面的實現中,檢查物件的類型是否為 "xml" 的目的在於解決新近版本的 JavaScript 中表達 XML 物件的特異之處。如果您想瞭解其中瑣碎細節,可以參考 {{ bug(634150) }}。</div>
<pre class="brush: js">
instanceOf (chris, Engineer)
instanceOf (chris, WorkerBee)
instanceOf (chris, Employee)
instanceOf (chris, Object)
</pre>
<p>但如下運算式為假:</p>
<pre class="brush: js">
instanceOf (chris, SalesPerson)</pre>
<h3 id=".E6.A7.8B.E9.80.A0.E5.99.A8.E4.B8.AD.E7.9A.84.E5.85.A8.E5.9F.9F.E8.B3.87.E8.A8.8A">構造器中的全域資訊</h3>
<p>在創建構造器時,在構造器中設置全域資訊要小心。例如,假設希望為每一個雇員分配一個唯一標識。可能會為 <code>Employee</code> 使用如下定義:</p>
<pre class="brush: js">
var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}
</pre>
<p>基於該定義,在創建新的 <code>Employee</code> 時,構造器為其分配了序列中的下一個識別字。然後遞增全域的識別字計數器。因此,如果,如果隨後的語句如下,則 <code>victoria.id</code> 為 1 而 <code>harry.id</code> 為 2:</p>
<pre class="brush: js">
var victoria = new Employee("Pigbert, Victoria", "pubs")
var harry = new Employee("Tschopik, Harry", "sales")
</pre>
<p>乍一看似乎沒問題。但是,無論什麼目的,在每一次創建 <code>Employee</code> 對象時,<code>idCounter</code> 都將被遞增一次。如果創建本章中所描述的整個 <code>Employee</code> 層級結構,每次設置原型的時候,<code>Employee</code> 構造器都將被調用一次。假設有如下代碼:</p>
<pre class="brush: js">
var idCounter = 1;

function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   this.id = idCounter++;
}

function Manager (name, dept, reports) {...}
Manager.prototype = new Employee;

function WorkerBee (name, dept, projs) {...}
WorkerBee.prototype = new Employee;

function Engineer (name, projs, mach) {...}
Engineer.prototype = new WorkerBee;

function SalesPerson (name, projs, quota) {...}
SalesPerson.prototype = new WorkerBee;

var mac = new Engineer("Wood, Mac");
</pre>
<p>還可以進一步假設上面省略掉的定義中包含 <code>base</code> 屬性而且調用了原型鏈中高於它們的構造器。即便在現在這個情況下,在 <code>mac</code> 物件創建時,<code>mac.id</code> 為 5。</p>
<p>依賴于應用程式,計數器額外的遞增可能有問題,也可能沒問題。如果確實需要準確的計數器,則以下構造器可以作為一個可行的方案:</p>
<pre class="brush: js">
function Employee (name, dept) {
   this.name = name || "";
   this.dept = dept || "general";
   if (name)
      this.id = idCounter++;
}
</pre>
<p>在用作原型而創建新的 <code>Employee</code> 實例時,不會指定參數。使用這個構造器定義,如果不指定參數,構造器不會指定識別字,也不會遞增計數器。而如果想讓 <code>Employee</code> 分配到識別字,則必需為雇員指定姓名。在這個例子中,<code>mac.id</code> 將為 1。</p>
<h3 id=".E6.B2.92.E6.9C.89.E5.A4.9A.E7.B9.BC.E6.89.BF">沒有多繼承</h3>
<p>某些物件導向語言支援多重繼承。也就是說,物件可以從無關的多個父物件中繼承屬性和屬性值。JavaScript 不支持多重繼承。</p>
<p>JavaScript 屬性值的繼承是在運行時通過檢索物件的原型鏈來實現的。因為物件只有一個原型與之關聯,所以 JavaScript 無法動態地從多個原型鏈中繼承。</p>
<p>在 JavaScript 中,可以在構造器函數中調用多個其它的構造器函數。這一點造成了多重繼承的假像。例如,考慮如下語句:</p>
<pre class="brush: js">
function Hobbyist (hobby) {
   this.hobby = hobby || "scuba";
}

function Engineer (name, projs, mach, hobby) {
   this.base1 = WorkerBee;
   this.base1(name, "engineering", projs);
   this.base2 = Hobbyist;
   this.base2(hobby);
   this.machine = mach || "";
}
Engineer.prototype = new WorkerBee;

var dennis = new Engineer("Doe, Dennis", ["collabra"], "hugo")
</pre>
<p>進一步假設使用本章前面所屬的 <code>WorkerBee</code> 的定義。此時 <code>dennis</code> 物件具有如下屬性:</p>
<pre class="brush: js">
dennis.name == "Doe, Dennis"
dennis.dept == "engineering"
dennis.projects == ["collabra"]
dennis.machine == "hugo"
dennis.hobby == "scuba"
</pre>
<p><code>dennis</code> 確實從 <code>Hobbyist</code> 構造器中獲得了 <code>hobby</code> 屬性。但是,假設添加了一個屬性到 <code>Hobbyist</code> 構造器的原型:</p>
<pre class="brush: js">
Hobbyist.prototype.equipment = ["mask", "fins", "regulator", "bcd"]
</pre>
<p><code>dennis</code> 物件不會繼承這個新屬性。</p>
<div>
  {{ PreviousNext("JavaScript/Guide/Predefined_Core_Objects", "JavaScript/Guide/Inheritance_Revisited") }}</div>
Revert to this revision