本文用作js對象學習的記錄。
JavaScript 是一種基于原型的面向對象語言,而不是基于類的。
基于類 vs 基于原型的語言
基于類的面向對象語言,比如 Java 和 C++,是構建在兩個不同實體的概念之上的:即類和實例。
類(class):定義了所有用于具有某一組特征對象的屬性(可以將 Java 中的方法和變量以及 C++ 中的成員都視作屬性)。類是抽象的事物,而不是其所描述的全部對象中的任何特定的個體。例如 Employee 類可以用來表示所有雇員的集合。
實例(instance):類的實例化體現;或者說,是類的一個成員。例如, Victoria 可以是 Employee 類的一個實例,表示一個特定的雇員個體。實例具有和其父類完全一致的屬性。
而基于原型的語言(如 JavaScript)并不存在這種區別:它只有對象。基于原型的語言具有所謂原型對象(prototypical object)的概念。原型對象可以作為一個模板,新對象可以從中獲得原始的屬性。任何對象都可以指定其自身的屬性,既可以是創建時也可以在運行時創建。而且,任何對象都可以作為另一個對象的原型(prototype),從而允許后者共享前者的屬性。**
基于類(Java)和基于原型(JavaScript)的對象系統的比較:
基于類的(Java)| 基于原型的(JavaScript)
-|-|-
通過類定義來定義類;通過構造器方法來實例化類 | 通過構造器函數來定義和創建一組對象
通過 new操作符創建單個對象 | 相同
通過類定義來定義現存類的子類,從而構建對象的層級結構 | 指定一個對象作為原型并且與構造函數一起構建對象的層級結構
遵循類鏈繼承屬性 | 遵循原型鏈繼承屬性
類定義指定類的所有實例的所有屬性,無法在運行時動態添加屬性 | 構造器函數或原型指定初始的屬性集,允許動態地向單個的對象或者整個對象集中添加或移除屬性
Employee 示例
余下部分將使用如下圖所示的 Employee 層級結構。
對象定義:
Manager 和 WorkerBee的定義表示在如何指定繼承鏈中上一層對象時,兩者存在不同點。在 JavaScript 中,您會添加一個原型實例作為構造器函數prototype屬性的值,而這一動作可以在構造器函數定義后的任意時刻執行。而在 Java 中,則需要在類定義中指定父類,且不能在類定義之外改變父類。
在對Engineer和 SalesPerson定義時,創建了繼承自 WorkerBee的對象,該對象會進而繼承自Employee。這些對象會具有在這個鏈之上的所有對象的屬性。另外,它們在定義時,又重載了繼承的 dept屬性值,賦予新的屬性值。
對象的屬性
-
繼承屬性
假設您通過如下語句創建一個 mark 對象作為 WorkerBee 的實例:
var mark = new WorkerBee;
當 JavaScript 發現* new 操作符時,它會創建一個通用(generic)對象,并將其作為關鍵字* this **的值傳遞給 WorkerBee 的構造器函數。該構造器函數顯式地設置 projects 屬性的值,然后隱式地將其內部的 __proto__ 屬性設置為WorkerBee.prototype的值(屬性的名稱前后均有兩個下劃線)。__proto__屬性決定了用于返回屬性值的原型鏈。一旦這些屬性設置完成,JavaScript 返回新創建的對象,然后賦值語句會將變量 mark 的值指向該對象。
這個過程不會顯式的將 mark 所繼承的原型鏈中的屬性值作為本地變量存放在 mark 對象中。當請求屬性的值時,JavaScript 將首先檢查對象自身中是否存在屬性的值,如果有,則返回該值。如果不存在,JavaScript會通過 __proto__ 對原型鏈進行檢查。如果原型鏈中的某個對象包含該屬性的值,則返回這個值。如果沒有找到該屬性,JavaScript 則認為對象中不存在該屬性。
這樣,mark對象中將具有如下的屬性和對應的值:
mark.name = "";
mark.dept = "general";
mark.projects = [];
mark對象從 mark.__proto__中保存的原型對象中繼承了 name和 dept 屬性的值。并由 WorkerBee 構造器函數為projects屬性設置了本地值。 這就是 JavaScript 中的屬性和屬性值的繼承。
由于這些構造器不支持為實例設置特定的值,所以這些屬性值僅僅是創建自 WorkerBee 的所有對象所共享的默認值。當然這些屬性的值是可以修改的,所以您可以為 mark指定特定的信息,如下所示:
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 的原型中重載該屬性的效果。
-
更加靈活的構造器
截至到現在,構造器函數都不允許在創建新的實例時指定屬性值。其實我們也可以像Java一樣,為構造器提供參數以初始化實例的屬性值。下圖即實現方式之一。
注意,由上面對類的定義,您無法為諸如 name 這樣的繼承屬性指定初始值。如果想在JavaScript中為繼承的屬性指定初始值,您需要在構造器函數中添加更多的代碼。
到目前為止,構造器函數已經能夠創建一個普通對象,然后為新對象指定本地的屬性和屬性值。您還可以通過直接調用原型鏈上的更高層次對象的構造器函數,讓構造器添加更多的屬性。下圖即實現了這一功能。
下面是 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 會有以下步驟:
- new 操作符創建了一個新的通用對象,并將其 __proto__ 屬性設置為 Engineer.prototype。
- new 操作符將該新對象作為 this 的值傳遞給 Engineer 構造器。
- 構造器為該新對象創建了一個名為 base 的新屬性,并指向 WorkerBee 的構造器。這使得 WorkerBee 構造器成為Engineer 對象的一個方法。base 屬性的名稱并沒有什么特殊性,我們可以使用任何其他合法的名稱來代替;base 僅僅是為了貼近它的用意。
- 構造器調用 base 方法,將傳遞給該構造器的參數中的兩個,作為參數傳遞給 base 方法,同時還傳遞一個字符串參數 "engineering"。顯式地在構造器中使用 "engineering" 表明所有 Engineer 對象繼承的 dept 屬性具有相同的值,且該值重載了繼承自 Employee 的值。
- 因為 base 是 Engineer 的一個方法,在調用 base 時,JavaScript 將在步驟 1 中創建的對象綁定給 this 關鍵字。這樣,WorkerBee 函數接著將 "Doe, Jane" 和 "engineering" 參數傳遞給 Employee 構造器函數。當從 Employee 構造器函數返回時,WorkerBee 函數用剩下的參數設置 projects 屬性。
- 當從 base 方法返回后,Engineer 構造器將對象的 machine 屬性初始化為 "belau"。
- 當從構造器返回時,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 將執行下面的步驟:
- 檢查本地值是否存在。如果存在,返回該值。
- 如果本地值不存在,檢查原型鏈(通過 __proto__ 屬性)。
- 如果原型鏈中的某個對象具有指定屬性的值,則返回該值。
- 如果這樣的屬性不存在,則對象沒有該屬性。
以上步驟的結果依賴于您是如何定義的。最早的例子中具有如下定義:
function Employee () {
this.name = "";
this.dept = "general";
}
function WorkerBee () {
this.projects = [];
}
WorkerBee.prototype = new Employee;
基于這些定義,假定通過如下的語句創建 WorkerBee 的實例 amy:
var amy = new WorkerBee;
則 amy 對象將具有一個本地屬性,projects。name 和 dept 屬性則不是 amy 對象本地的,而是從 amy 對象的 __proto__ 屬性獲得的。因此,amy 將具有如下的屬性值:
amy.name == "";
amy.dept == "general";
amy.projects == [];
現在,假設修改了與 Employee 的相關聯原型中的 name 屬性的值:
Employee.prototype.name = "Unknown"
乍一看,您可能覺得新的值會傳播給所有 Employee 的實例。然而,并非如此。
在創建 Employee 對象的任意實例時,該實例的 name 屬性將獲得一個本地值(空的字符串)。這就意味著在創建一個新的Employee 對象作為 WorkerBee 的原型時,WorkerBee.prototype 的 name 屬性將具有一個本地值。因此,當 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";
在這種情況下,amy 的 name 屬性將為 "Unknown"。
正如這些例子所示,如果希望對象的屬性具有默認值,并且希望在運行時修改這些默認值,應該在對象的原型中設置這些屬性,而不是在構造器函數中。
判斷實例的關系
JavaScript 的屬性查找機制首先在對象自身的屬性中查找,如果指定的屬性名稱沒有找到,將在對象的特殊屬性 __proto__ 中查找。這個過程是遞歸的;被稱為“在原型鏈中查找”。
特殊的 __proto__ 屬性是在構建對象時設置的;設置為構造器的 prototype 屬性的值。所以表達式 new Foo() 將創建一個對象,其__proto__ == Foo.prototype。因而,修改 Foo.prototype 的屬性,將改變所有通過 new Foo() 創建的對象的屬性的查找。
每個對象都有一個 __proto__ 對象屬性(除了 Object);每個函數都有一個 prototype 對象屬性。因此,通過“原型繼承(prototype inheritance)”,對象與其它對象之間形成關系。通過比較對象的 __proto__ 屬性和函數的 prototype 屬性可以檢測對象的繼承關系。
JavaScript 提供了便捷方法:instanceof 操作符可以用來將一個對象和一個函數做檢測,如果對象繼承自函數的原型,則該操作符返回真。例如我們使用和在繼承屬性中相同的一組定義。創建 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;
}
構造器中的全局信息
在創建構造器時,在構造器中設置全局信息要小心。例如,假設希望為每一個雇員分配一個唯一標識。可能會為 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 構造器都將被調用一次。
依賴于應用程序,計數器額外的遞增可能有問題,也可能沒問題。如果確實需要準確的計數器,則以下構造器可以作為一個可行的方案:
function Employee (name, dept) {
this.name = name || "";
this.dept = dept || "general";
if (name)
this.id = idCounter++;
}
在用作原型而創建新的 Employee 實例時,不會指定參數。使用這個構造器定義,如果不指定參數,構造器不會指定標識符,也不會遞增計數器。而如果想讓 Employee 分配到標識符,則必需為雇員指定姓名。在這個例子中,mac.id 將為 1。
沒有多繼承
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 對象不會繼承這個新屬性。