JavaScript-理解 面向物件的程式設計
本文結構
建立物件
物件的繼承(有些不是很理解-後續會更新)
官方定義:物件是擁有屬性和方法的資料。
物件和函式比較: 函式是用來實現具體功能的程式碼,用一種方式把他們組織起來,就是函數了。 物件是有屬性和方法的一個東西,在物件中的函式就變成了方法。
細節比較 物件同樣是對js程式碼封裝,不過物件可以封裝函式(方法)。
比如把某一類的函式(方法)都封裝到某個物件中。 這樣可以系統的管理呼叫函式(方法)。
- 比如我寫了很多的函式,只要知道我想要呼叫的函式是哪一類的。
- 宣告相應的物件,就可以很容易的找到我要呼叫的函式(方法)。
- 物件中的屬性就是變數,物件中的方法就是函式
物件在 JavaScript 中被稱為引用型別的值,而且有一些內建的引用型別可以用來建立特定的物件,
現簡要總結如下:
引用型別與傳統面向物件程式設計中的類相似,但實現不同;
- Object 是一個基礎型別,其他所有型別都從 Object 繼承了基本的行為;
- Array 型別是一組值的有序列表,同時還提供了操作和轉換這些值的功能;
- Date 型別提供了有關日期和時間的資訊,包括當前日期和時間以及相關的計算功能;
- RegExp 型別是 ECMAScript支援正則表示式的一個介面,提供了基本的和一些高階的正則表 達式功能。
函式實際上是 Function 型別的例項,因此函式也是物件;而這一點正是 JavaScript有特色的地 方。由於函式是物件,所以函式也擁有方法,可以用來增強其行為。
因為有了基本包裝型別,所以 JavaScript 中的基本型別值可以被當作物件來訪問。三種基本包裝類 型分別是:Boolean、Number 和 String。以下是它們共同的特徵:
- 每個包裝型別都對映到同名的基本型別;
- 在讀取模式下訪問基本型別值時,就會建立對應的基本包裝型別的一個物件,從而方便了資料 操作;
- 操作基本型別值的語句一經執行完畢,就會立即銷燬新建立的包裝物件。
在所有程式碼執行之前,作用域中就已經存在兩個內建物件:Global 和 Math。在大多數ECMAScript 實現中都不能直接訪問 Global 物件;不過,Web 瀏覽器實現了承擔該角色的 window 物件。全域性變 量和函式都是 Global 物件的屬性。Math 物件提供了很多屬性和方法,用於輔助完成複雜的數學計算 任務。
ECMAScript支援面向物件(OO)程式設計,但不使用類或者介面。物件可以在程式碼執行過程中建立和 增強,因此具有動態性而非嚴格定義的實體。在沒有類的情況下,可以採用下列模式建立物件。
工廠模式,使用簡單的函式建立物件,為物件新增屬性和方法,然後返回物件。這個模式後來 被建構函式模式所取代。
建構函式模式,可以建立自定義引用型別,可以像建立內建物件例項一樣使用 new 操作符。不 過,建構函式模式也有缺點,即它的每個成員都無法得到複用,包括函式。由於函式可以不局 限於任何物件(即與物件具有鬆散耦合的特點),因此沒有理由不在多個物件間共享函式。
原型模式,使用建構函式的 prototype 屬性來指定那些應該共享的屬性和方法。組合使用構造 函式模式和原型模式時,使用建構函式定義例項屬性,而使用原型定義共享的屬性和方法。
JavaScript 主要通過原型鏈實現繼承。原型鏈的構建是通過將一個型別的例項賦值給另一個構造函 數的原型實現的。這樣,子型別就能夠訪問超型別的所有屬性和方法,這一點與基於類的繼承很相似。 原型鏈的問題是物件例項共享所有繼承的屬性和方法,因此不適宜單獨使用。解決這個問題的技術是借 用建構函式,即在子型別建構函式的內部呼叫超型別建構函式。這樣就可以做到每個例項都具有自己的 屬性,同時還能保證只使用建構函式模式來定義型別。使用多的繼承模式是組合繼承,這種模式使用 原型鏈繼承共享的屬性和方法,而通過借用建構函式繼承例項屬性。
此外,還存在下列可供選擇的繼承模式。
原型式繼承,可以在不必預先定義建構函式的情況下實現繼承,其本質是執行對給定物件的淺 複製。而複製得到的副本還可以得到進一步改造。
寄生式繼承,與原型式繼承非常相似,也是基於某個物件或某些資訊建立一個物件,然後增強 物件,後返回物件。為了解決組合繼承模式由於多次呼叫超型別建構函式而導致的低效率問 題,可以將這個模式與組合繼承一起使用。
寄生組合式繼承,集寄生式繼承和組合繼承的優點與一身,是實現基於型別繼承的有效方式。
面向物件(Object-Oriented,OO)的語言有一個標誌,那就是它們都有類的概念,而通過類可 以建立任意多個具有相同屬性和方法的物件。前面提到過,ECMAScript中沒有類的概念,因 此它的物件也與基於類的語言中的物件有所不同。
ECMA-262把物件定義為:“無序屬性的集合,其屬性可以包含基本值、物件或者函式。”嚴格來講, 這就相當於說物件是一組沒有特定順序的值。物件的每個屬性或方法都有一個名字,而每個名字都對映 到一個值。正因為這樣(以及其他將要討論的原因),我們可以把 ECMAScript的物件想象成散列表:無 非就是一組名值對,其中值可以是資料或函式。 每個物件都是基於一個引用型別建立的,這個引用型別可以是第 5章討論的原生型別,也可以是開 發人員定義的型別
單體內建物件
ECMA-262對內建物件的定義是:“由 ECMAScript實現提供的、不依賴於宿主環境的物件,這些對 象在 ECMAScript程式執行之前就已經存在了。”意思就是說,開發人員不必顯式地例項化內建物件,因為它們已經例項化了。前面我們已經介紹了大多數內建物件,例如 Object、Array 和 String。 ECMA-262還定義了兩個單體內建物件:Global 和 Math。
Global物件
Global(全域性)物件可以說是 ECMAScript中特別的一個物件了,因為不管你從什麼角度上看, 這個物件都是不存在的。ECMAScript中的 Global 物件在某種意義上是作為一個終極的“兜底兒物件” 來定義的。換句話說,不屬於任何其他物件的屬性和方法,終都是它的屬性和方法。事實上,沒有全 局變數或全域性函式;所有在全域性作用域中定義的屬性和函式,都是 Global 物件的屬性。本書前面介紹 過的那些函式,諸如 isNaN()、isFinite()、parseInt()以及 parseFloat(),實際上全都是 Global 物件的方法。除此之外,Global 物件還包含其他一些方法。
1. URI編碼方法
Global 物件的 encodeURI()和 encodeURIComponent()方法可以對 URI(Uniform Resource Identifiers,通用資源識別符號)進行編碼,以便傳送給瀏覽器。有效的 URI 中不能包含某些字元,例如 空格。而這兩個 URI編碼方法就可以對 URI進行編碼,它們用特殊的 UTF-8編碼替換所有無效的字元, 從而讓瀏覽器能夠接受和理解。
其中,encodeURI()主要用於整個 URI(例如,http://www.wrox.com/illegal value.htm),而 encode- URIComponent()主要用於對 URI中的某一段(例如前面 URI中的 illegal value.htm)進行編碼。 它們的主要區別在於,encodeURI()不會對本身屬於 URI 的特殊字元進行編碼,例如冒號、正斜槓、 問號和井字號;而 encodeURIComponent()則會對它發現的任何非標準字元進行編碼。來看下面的例子。
var uri = "http://www.wrox.com/illegal value.htm#start";
//"http://www.wrox.com/illegal%20value.htm#start"
alert(encodeURI(uri));
//"http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start"
alert(encodeURIComponent(uri));
使用 encodeURI()編碼後的結果是除了空格之外的其他字元都原封不動,只有空格被替換成了 %20。而 encodeURIComponent()方法則會使用對應的編碼替換所有非字母數字字元。這也正是可以 對整個URI使用encodeURI(),而只能對附加在現有URI後面的字串使用encodeURIComponent() 的原因所在。
一般來說,我們使用 encodeURIComponent() 方法的時候要比使用 encodeURI()更多,因為在實踐中更常見的是對查詢字串引數而不是對基礎 URI 進行編碼
與 encodeURI()和 encodeURIComponent()方法對應的兩個方法分別是 decodeURI()和 decodeURIComponent()。其中,decodeURI()只能對使用 encodeURI()替換的字元進行解碼。例如, 它可將%20 替換成一個空格,但不會對%23 作任何處理,因為%23 表示井字號(#),而井字號不是使用 encodeURI()替換的。同樣地,decodeURIComponent()能夠解碼使用 encodeURIComponent()編碼的所有字元,即它可以解碼任何特殊字元的編碼。來看下面的例子:
var uri = "http%3A%2F%2Fwww.wrox.com%2Fillegal%20value.htm%23start";
//http%3A%2F%2Fwww.wrox.com%2Fillegal value.htm%23start
alert(decodeURI(uri));
//http://www.wrox.com/illegal value.htm#start
alert(decodeURIComponent(uri));
這裡,變數 uri 包含著一個由 encodeURIComponent()編碼的字串。在第一次呼叫 decodeURI() 輸出的結果中,只有%20 被替換成了空格。而在第二次呼叫 decodeURIComponent()輸出的結果中, 所有特殊字元的編碼都被替換成了原來的字元,得到了一個未經轉義的字串(但這個字串並不是一 個有效的 URI)
URI方法 encodeURI()、encodeURIComponent()、decodeURI()和 decode- URIComponent()用於替代已經被ECMA-262第3版廢棄的escape()和unescape() 方法。URI方法能夠編碼所有 Unicode字元,而原來的方法只能正確地編碼 ASCII字元。 因此在開發實踐中,特別是在產品級的程式碼中,一定要使用URI方法,不要使用 escape() 和unescape()方法。
2. eval()方法
現在,我們介紹後一個——大概也是整個ECMAScript語言中強大的一個方法:eval()。eval() 方法就像是一個完整的 ECMAScript解析器,它只接受一個引數,即要執行的 ECMAScript(或 JavaScript) 字串。看下面的例子:
eval("alert('hi')");
這行程式碼的作用等價於下面這行程式碼:
alert("hi");
當解析器發現程式碼中呼叫 eval()方法時,它會將傳入的引數當作實際的 ECMAScript語句來解析, 然後把執行結果插入到原位置。通過 eval()執行的程式碼被認為是包含該次呼叫的執行環境的一部分, 因此被執行的程式碼具有與該執行環境相同的作用域鏈。這意味著通過 eval()執行的程式碼可以引用在包 含環境中定義的變數,舉個例子:
var msg = "hello world";
eval("alert(msg)"); //"hello world"
可見,變數 msg 是在 eval()呼叫的環境之外定義的,但其中呼叫的 alert()仍然能夠顯示"hello world"。這是因為上面第二行程式碼終被替換成了一行真正的程式碼。同樣地,我們也可以在 eval() 呼叫中定義一個函式,然後再在該呼叫的外部程式碼中引用這個函式:
eval("function sayHi() {alert('hi'); }");
sayHi();
顯然,函式 sayHi()是在 eval()內部定義的。但由於對 eval()的呼叫終會被替換成定義函式 的實際程式碼,因此可以在下一行呼叫 sayHi()。對於變數也一樣:
eval("var msg = 'hello world'; ");
alert(msg); //"hello world"
在 eval()中建立的任何變數或函式都不會被提升,因為在解析程式碼的時候,它們被包含在一個字 符串中;它們只在 eval()執行的時候建立。 嚴格模式下,在外部訪問不到 eval()中建立的任何變數或函式,因此前面兩個例子都會導致錯誤。 同樣,在嚴格模式下,為 eval 賦值也會導致錯誤:
"use strict";
eval = "hi"; //causes error
能夠解釋程式碼字串的能力非常強大,但也非常危險。因此在使用 eval()時必 須極為謹慎,特別是在用它執行使用者輸入資料的情況下。否則,可能會有惡意使用者輸 入威脅你的站點或應用程式安全的程式碼(即所謂的程式碼注入)。
3. Global 物件的屬性
Global 物件還包含一些屬性,其中一部分屬性已經在本書前面介紹過了。例如,特殊的值 undefined、NaN 以及 Infinity 都是 Global 物件的屬性。此外,所有原生引用型別的建構函式,像 Object 和 Function,也都是 Global 物件的屬性。下表列出了 Global 物件的所有屬性。
ECMAScript 5明確禁止給 undefined、NaN 和 Infinity 賦值,這樣做即使在非嚴格模式下也會 導致錯誤。
4. window 物件
ECMAScript 雖然沒有指出如何直接訪問 Global 物件,但 Web 瀏覽器都是將這個全域性物件作為 window 物件的一部分加以實現的。因此,在全域性作用域中宣告的所有變數和函式,就都成為了 window 物件的屬性。來看下面的例子。
var color = "red";
function sayColor(){
alert(window.color);
}
window.sayColor(); //"red"
這裡定義了一個名為color的全域性變數和一個名為sayColor()的全域性函式。在sayColor()內部, 我們通過 window.color 來訪問 color 變數,以說明全域性變數是 window 物件的屬性。然後,又使用 window.sayColor()來直接通過 window 物件呼叫這個函式,結果顯示在了警告框中。
JavaScript中的window物件除了扮演ECMAScript規定的Global物件的角色外, 還承擔了很多別的任務。第 8章在討論瀏覽器物件模型時將詳細介紹 window 物件。
另一種取得 Global 物件的方法是使用以下程式碼:
var global = function(){
return this;
}();
以上程式碼建立了一個立即呼叫的函式表示式,返回 this 的值。如前所述,在沒有給函式明確指定 this 值的情況下(無論是通過將函式新增為物件的方法,還是通過呼叫 call()或 apply()),this 值等於 Global 物件。而像這樣通過簡單地返回 this 來取得 Global 物件,在任何執行環境下都是可 行的。第 7章將深入討論函式表示式。
Math物件
ECMAScript還為儲存數學公式和資訊提供了一個公共位置,即 Math 物件。與我們在 JavaScript直 接編寫的計算功能相比,Math 物件提供的計算功能執行起來要快得多。Math 物件中還提供了輔助完成 這些計算的屬性和方法
1. Math 物件的屬性
Math 物件包含的屬性大都是數學計算中可能會用到的一些特殊值。下表列出了這些屬性。
雖然討論這些值的含義和用途超出了本書範圍,但你確實可以隨時使用它們。
2. min()和 max()方法
Math 物件還包含許多方法,用於輔助完成簡單和複雜的數學計算。 其中,min()和 max()方法用於確定一組數值中的小值和大值。這兩個方法都可以接收任意多 個數值引數,如下面的例子所示。
var max = Math.max(3, 54, 32, 16);
alert(max); //54
var min = Math.min(3, 54, 32, 16);
alert(min); //3
對於 3、54、32和 16,Math.max()返回 54,而 Math.min()返回 3。這兩個方法經常用於避免多 餘的迴圈和在 if 語句中確定一組數的大值。 要找到陣列中的大或小值,可以像下面這樣使用 apply()方法。
var values = [1, 2, 3, 4, 5, 6, 7, 8];
var max = Math.max.apply(Math, values);
這個技巧的關鍵是把 Math 物件作為 apply()的第一個引數,從而正確地設定 this 值。然後,可 以將任何陣列作為第二個引數。
3. 舍入方法
下面來介紹將小數值舍入為整數的幾個方法:Math.ceil()、Math.floor()和 Math.round()。 這三個方法分別遵循下列舍入規則:
- Math.ceil()執行向上舍入,即它總是將數值向上舍入為接近的整數;
- Math.floor()執行向下舍入,即它總是將數值向下舍入為接近的整數;
- Math.round()執行標準舍入,即它總是將數值四捨五入為接近的整數(這也是我們在數學課 上學到的舍入規則)。
下面是使用這些方法的示例:
alert(Math.ceil(25.9)); //26
alert(Math.ceil(25.5)); //26
alert(Math.ceil(25.1)); //26
alert(Math.round(25.9)); //26
alert(Math.round(25.5)); //26
alert(Math.round(25.1)); //25
alert(Math.floor(25.9)); //25
alert(Math.floor(25.5)); //25
alert(Math.floor(25.1)); //25
對於所有介於 25和 26(不包括 26)之間的數值,Math.ceil()始終返回 26,因為它執行的是向 上舍入。Math.round()方法只在數值大於等於 25.5 時返回 26;否則返回 25。後,Math.floor() 對所有介於 25和 26(不包括 26)之間的數值都返回 25。
4. random()方法
Math.random()方法返回大於等於 0小於 1的一個隨機數。對於某些站點來說,這個方法非常實用, 因為可以利用它來隨機顯示一些名人名言和新聞事件。套用下面的公式,就可以利用 Math.random() 從某個整數範圍內隨機選擇一個值。
值 = Math.floor(Math.random() * 可能值的總數 + 第一個可能的值)
公式中用到了 Math.floor()方法,這是因為 Math.random()總返回一個小數值。而用這個小數 值乘以一個整數,然後再加上一個整數,終結果仍然還是一個小數。舉例來說,如果你想選擇一個 1 到 10之間的數值,可以像下面這樣編寫程式碼:
var num = Math.floor(Math.random() * 10 + 1);
總共有 10個可能的值(1到 10),而第一個可能的值是 1。而如果想要選擇一個介於 2到 10之間的 值,就應該將上面的程式碼改成這樣:
var num = Math.floor(Math.random() * 9 + 2);
從 2數到 10要數 9個數,因此可能值的總數就是 9,而第一個可能的值就是 2。多數情況下,其實 都可以通過一個函式來計算可能值的總數和第一個可能的值,例如:
function selectFrom(lowerValue, upperValue) {
var choices = upperValue - lowerValue + 1;
return Math.floor(Math.random() * choices + lowerValue);
}
var num = selectFrom(2, 10); alert(num);
// 介於 2 和 10 之間(包括 2 和 10)的一個數值
函式 selectFrom()接受兩個引數:應該返回的小值和大值。而用大值減小值再加 1得到 了可能值的總數,然後它又把這些數值套用到了前面的公式中。這樣,通過呼叫 selectFrom(2,10) 就可以得到一個介於 2和 10之間(包括 2和 10)的數值了。利用這個函式,可以方便地從陣列中隨機 取出一項,例如
var colors = ["red", "green", "blue", "yellow", "black", "purple", "brown"];
var color = colors[selectFrom(0, colors.length-1)];
alert(color);
// 可能是陣列中包含的任何一個字串
在這個例子中,傳遞給 selectFrom()的第二個引數是陣列的長度減1,也就是陣列中後一項的位置。
5. 其他方法
Math 物件中還包含其他一些與完成各種簡單或複雜計算有關的方法,但詳細討論其中每一個方法 的細節及適用情形超出了本書的範圍。下面我們就給出一個表格,其中列出了這些沒有介紹到的 Math 物件的方法。
雖然 ECMA-262規定了這些方法,但不同實現可能會對這些方法採用不同的演算法。畢竟,計算某個 值的正弦、餘弦和正切的方式多種多樣。也正因為如此,這些方法在不同的實現中可能會有不同的精度。
物件屬性
建立物件
雖然 Object 建構函式或物件字面量都可以用來建立單個物件,但這些方式有個明顯的缺點:使用同 一個介面建立很多物件,會產生大量的重複程式碼。(例如一個物件有個方法為輸出顯示固定值,那麼每次使用該介面建立物件時,都要重複使用那個方法,從而造成程式碼重複)
為解決這個問題,人們開始使用工廠模式的一種變體
工廠模式 (利用方法制造一個專屬物件)
.工廠模式是軟體工程領域一種廣為人知的設計模式,這種模式抽象了建立具體物件的過程(本書後 面還將討論其他設計模式及其在 JavaScript中的實現)。考慮到在 ECMAScript中無法建立類,開發人員 就發明了一種函式,用函式來封裝以特定介面建立物件的細節,如下面的例子所示。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
函式 createPerson()能夠根據接受的引數來構建一個包含所有必要資訊的 Person 物件。可以無 數次地呼叫這個函式,而每次它都會返回一個包含三個屬性一個方法的物件。工廠模式雖然解決了建立 多個相似物件的問題,但卻沒有解決物件識別的問題(即怎樣知道一個物件的型別)。
隨著 JavaScript 的發展,又一個新模式出現了。
建構函式模式
ECMAScript中的建構函式可用來建立特定型別的物件。像 Object 和 Array 這樣的原生建構函式,在執行時會自動出現在執行環境中。此外,也可以建立自定義的建構函式,從而定義 自定義物件型別的屬性和方法。例如,可以使用建構函式模式將前面的例子重寫如下。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個例子中,Person()函式取代了 createPerson()函式。我們注意到,Person()中的程式碼 除了與 createPerson()中相同的部分外,還存在以下不同之處:
- 沒有顯式地建立物件;
- 直接將屬性和方法賦給了 this 物件;
- 沒有 return 語句。
此外,還應該注意到函式名 Person 使用的是大寫字母 P。
按照慣例,建構函式始終都應該以一個 大寫字母開頭,而非建構函式則應該以一個小寫字母開頭。這個做法借鑑自其他 OO語言,主要是為了 區別於 ECMAScript中的其他函式;因為建構函式本身也是函式,只不過可以用來建立物件而已。 要建立 Person 的新例項,必須使用 new 操作符。以這種方式呼叫建構函式實際上會經歷以下 4 個步驟:
(1) 建立一個新物件;
(2) 將建構函式的作用域賦給新物件(因此 this 就指向了這個新物件);
(3) 執行建構函式中的程式碼(為這個新物件新增屬性);
(4) 返回新物件。
物件的 constructor 屬性初是用來標識物件型別的。但是,提到檢測物件型別,還是 instan- ceof 操作符要更可靠一些。我們在這個例子中建立的所有物件既是 Object 的例項,同時也是 Person 的例項,這一點通過 instanceof 操作符可以得到驗證
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
alert(person2 instanceof Object); //true
alert(person2 instanceof Person); //true
建立自定義的建構函式意味著將來可以將它的例項標識為一種特定的型別;而這正是建構函式模式 勝過工廠模式的地方。在這個例子中,person1 和 person2 之所以同時是 Object 的例項,是因為所 有物件均繼承自 Object(詳細內容稍後討論)。
1. 將建構函式當作函式
建構函式與其他函式的唯一區別,就在於呼叫它們的方式不同。不過,建構函式畢竟也是函式,不 存在定義建構函式的特殊語法。任何函式,只要通過 new 操作符來呼叫,那它就可以作為建構函式;而 任何函式,如果不通過 new 操作符來呼叫,那它跟普通函式也不會有什麼兩樣。例如,前面例子中定義 的 Person()函式可以通過下列任何一種方式來呼叫。
// 當作建構函式使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
// 作為普通函式呼叫
Person("Greg", 27, "Doctor"); // 新增到
window window.sayName(); //"Greg"
// 在另一個物件的作用域中呼叫
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); //"Kristen"
這個例子中的前兩行程式碼展示了建構函式的典型用法,即使用 new 操作符來建立一個新物件。接下 來的兩行程式碼展示了不使用new操作符呼叫Person()會出現什麼結果:屬性和方法都被新增給window 物件了。有讀者可能還記得,當在全域性作用域中呼叫一個函式時,this 物件總是指向 Global 物件(在 瀏覽器中就是 window 物件)。因此,在呼叫完函式之後,可以通過 window 物件來呼叫 sayName()方 法,並且還返回了"Greg"。後,也可以使用 call()(或者 apply())在某個特殊物件的作用域中 呼叫Person()函式。這裡是在物件o的作用域中呼叫的,因此呼叫後o就擁有了所有屬性和sayName() 方法。
2. 建構函式的問題
建構函式模式雖然好用,但也並非沒有缺點。使用建構函式的主要問題,就是每個方法都要在每個 例項上重新建立一遍。在前面的例子中,person1 和 person2 都有一個名為 sayName()的方法,但那 兩個方法不是同一個 Function 的例項。不要忘了——ECMAScript中的函式是物件,因此每定義一個 函式,也就是例項化了一個物件。從邏輯角度講,此時的建構函式也可以這樣定義。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)");
// 與宣告函式在邏輯上是等價的 }
從這個角度上來看建構函式,更容易明白每個 Person 例項都包含一個不同的 Function 例項(以 顯示 name 屬性)的本質。說明白些,以這種方式建立函式,會導致不同的作用域鏈和識別符號解析,但 建立 Function 新例項的機制仍然是相同的。因此,不同例項上的同名函式是不相等的,以下程式碼可以 證明這一點。
alert(person1.sayName == person2.sayName); //false
然而,建立兩個完成同樣任務的 Function 例項的確沒有必要;況且有 this 物件在,根本不用在 執行程式碼前就把函式繫結到特定物件上面。因此,大可像下面這樣,通過把函式定義轉移到建構函式外 部來解決這個問題。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個例子中,我們把 sayName()函式的定義轉移到了建構函式外部。而在建構函式內部,我們 將 sayName 屬性設定成等於全域性的 sayName 函式。這樣一來,由於 sayName 包含的是一個指向函式 的指標,因此 person1 和 person2 物件就共享了在全域性作用域中定義的同一個 sayName()函式。這 樣做確實解決了兩個函式做同一件事的問題,可是新問題又來了:在全域性作用域中定義的函式實際上只 能被某個物件呼叫,這讓全域性作用域有點名不副實。而更讓人無法接受的是:如果物件需要定義很多方 法,那麼就要定義很多個全域性函式,於是我們這個自定義的引用型別就絲毫沒有封裝性可言了。好在, 這些問題可以通過使用原型模式來解決。
原型模式
我們建立的每個函式都有一個 prototype(原型)屬性,這個屬性是一個指標,指向一個物件, 而這個物件的用途是包含可以由特定型別的所有例項共享的屬性和方法。如果按照字面意思來理解,那 麼 prototype 就是通過呼叫建構函式而建立的那個物件例項的原型物件。使用原型物件的好處是可以 讓所有物件例項共享它所包含的屬性和方法。換句話說,不必在建構函式中定義物件例項的資訊,而是 可以將這些資訊直接新增到原型物件中,如下面的例子所示。
function Person(){
}
Person.prototype.name = "Nicholas"; Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
在此,我們將 sayName()方法和所有屬性直接新增到了 Person 的 prototype 屬性中,建構函式 變成了空函式。即使如此,也仍然可以通過呼叫建構函式來建立新物件,而且新物件還會具有相同的屬 性和方法。但與建構函式模式不同的是,新物件的這些屬性和方法是由所有例項共享的。換句話說, person1 和 person2 訪問的都是同一組屬性和同一個 sayName()函式。要理解原型模式的工作原理, 必須先理解 ECMAScript中原型物件的性質。
1. 理解原型物件
無論什麼時候,只要建立了一個新函式,就會根據一組特定的規則為該函式建立一個 prototype 屬性,這個屬性指向函式的原型物件。在預設情況下,所有原型物件都會自動獲得一個 constructor (建構函式)屬性,這個屬性包含一個指向 prototype 屬性所在函式的指標。就拿前面的例子來說, Person.prototype. constructor 指向 Person。而通過這個建構函式,我們還可繼續為原型物件 新增其他屬性和方法。
建立了自定義的建構函式之後,其原型物件預設只會取得 constructor 屬性;至於其他方法,則 都是從 Object 繼承而來的。當呼叫建構函式建立一個新例項後,該例項的內部將包含一個指標(內部 屬性),指向建構函式的原型物件。ECMA-262第 5版中管這個指標叫[[Prototype]]。雖然在指令碼中 沒有標準的方式訪問[[Prototype]],但 Firefox、Safari 和 Chrome 在每個物件上都支援一個屬性 __proto__;而在其他實現中,這個屬性對指令碼則是完全不可見的。不過,要明確的真正重要的一點就 是,這個連線存在於例項與建構函式的原型物件之間,而不是存在於例項與建構函式之間。
以前面使用 Person 建構函式和 Person.prototype 建立例項的程式碼為例,圖 6-1展示了各個對 象之間的關係。
圖 6-1展示了 Person 建構函式、Person 的原型屬性以及 Person 現有的兩個例項之間的關係。 在此,Person.prototype指向了原型物件,而Person.prototype.constructor又指回了Person。 原型物件中除了包含 constructor 屬性之外,還包括後來新增的其他屬性。Person 的每個例項—— person1 和 person2 都包含一個內部屬性,該屬性僅僅指向了 Person.prototype;換句話說,它們 與建構函式沒有直接的關係。此外,要格外注意的是,雖然這兩個例項都不包含屬性和方法,但我們卻可以呼叫 person1.sayName()。這是通過查詢物件屬性的過程來實現的。
雖然在所有實現中都無法訪問到[[Prototype]],但可以通過 isPrototypeOf()方法來確定物件之 間是否存在這種關係。從本質上講,如果[[Prototype]]指向呼叫 isPrototypeOf()方法的物件 (Person.prototype),那麼這個方法就返回 true,如下所示:
alert(Person.prototype.isPrototypeOf(person1)); //true
alert(Person.prototype.isPrototypeOf(person2)); //true
這裡,我們用原型物件的 isPrototypeOf()方法測試了 person1 和 person2。因為它們內部都 有一個指向 Person.prototype 的指標,因此都返回了 true。 ECMAScript 5增加了一個新方法,叫 Object.getPrototypeOf(),在所有支援的實現中,這個 方法返回[[Prototype]]的值。例如:
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"Nicholas"
這裡的第一行程式碼只是確定 Object.getPrototypeOf()返回的物件實際就是這個物件的原型。 第二行程式碼取得了原型物件中 name 屬性的值,也就是"Nicholas"。使用 Object.getPrototypeOf() 可以方便地取得一個物件的原型,而這在利用原型實現繼承(本章稍後會討論)的情況下是非常重要的。 支援這個方法的瀏覽器有 IE9+、Firefox 3.5+、Safari 5+、Opera 12+和 Chrome。
每當程式碼讀取某個物件的某個屬性時,都會執行一次搜尋,目標是具有給定名字的屬性。搜尋首先 從物件例項本身開始。如果在例項中找到了具有給定名字的屬性,則返回該屬性的值;如果沒有找到, 則繼續搜尋指標指向的原型物件,在原型物件中查詢具有給定名字的屬性。如果在原型物件中找到了這 個屬性,則返回該屬性的值。也就是說,在我們呼叫 person1.sayName()的時候,會先後執行兩次搜 索。首先,解析器會問:“例項 person1 有 sayName 屬性嗎?”答:“沒有。”然後,它繼續搜尋,再 問:“person1 的原型有 sayName 屬性嗎?”答:“有。”於是,它就讀取那個儲存在原型物件中的函 數。當我們呼叫 person2.sayName()時,將會重現相同的搜尋過程,得到相同的結果。而這正是多個 物件例項共享原型所儲存的屬性和方法的基本原理。
前面提到過,原型最初只包含 constructor 屬性,而該屬性也是共享的,因此 可以通過物件例項訪問。
雖然可以通過物件例項訪問儲存在原型中的值,但卻不能通過物件例項重寫原型中的值。如果我們 在例項中添加了一個屬性,而該屬性與例項原型中的一個屬性同名,那我們就在例項中建立該屬性,該 屬性將會遮蔽原型中的那個屬性。來看下面的例子。
function Person(){
}
Person.prototype.name = "Nicholas"; Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg"; alert(person1.name);
//"Greg"——來自例項
alert(person2.name); //"Nicholas"——來自原型
在這個例子中,person1 的 name 被一個新值給遮蔽了。但無論訪問 person1.name 還是訪問 person2.name 都能夠正常地返回值,即分別是"Greg"(來自物件例項)和"Nicholas"(來自原型)。 當在 alert()中訪問 person1.name 時,需要讀取它的值,因此就會在這個例項上搜索一個名為 name 的屬性。這個屬性確實存在,於是就返回它的值而不必再搜尋原型了。當以同樣的方式訪問 person2. name 時,並沒有在例項上發現該屬性,因此就會繼續搜尋原型,結果在那裡找到了 name 屬性。
當為物件例項新增一個屬性時,這個屬性就會遮蔽原型物件中儲存的同名屬性;換句話說,新增這 個屬性只會阻止我們訪問原型中的那個屬性,但不會修改那個屬性。即使將這個屬性設定為 null,也 只會在例項中設定這個屬性,而不會恢復其指向原型的連線。不過,使用 delete 操作符則可以完全刪 除例項屬性,從而讓我們能夠重新訪問原型中的屬性,如下所示。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg"; alert(person1.name);
//"Greg"——來自例項
alert(person2.name); //"Nicholas"——來自原型
delete person1.name; alert(person1.name);
//"Nicholas"——來自原型
在這個修改後的例子中,我們使用 delete 操作符刪除了 person1.name,之前它儲存的"Greg" 值遮蔽了同名的原型屬性。把它刪除以後,就恢復了對原型中 name 屬性的連線。因此,接下來再呼叫 person1.name 時,返回的就是原型中 name 屬性的值了。
使用 hasOwnProperty()方法可以檢測一個屬性是存在於例項中,還是存在於原型中。這個方法(不 要忘了它是從 Object 繼承來的)只在給定屬性存在於物件例項中時,才會返回 true。來看下面這個例子。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg"; alert(person1.name); //"Greg"——來自例項
alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——來自原型
alert(person2.hasOwnProperty("name")); //false
delete person1.name; alert(person1.name); //"Nicholas"——來自原型
alert(person1.hasOwnProperty("name")); //false
通過使用 hasOwnProperty()方法,什麼時候訪問的是例項屬性,什麼時候訪問的是原型屬性就 一清二楚了。呼叫 person1.hasOwnProperty( "name")時,只有當 person1 重寫 name 屬性後才會 返回 true,因為只有這時候 name 才是一個例項屬性,而非原型屬性。圖 6-2展示了上面例子在不同情 況下的實現與原型的關係(為了簡單起見,圖中省略了與 Person 建構函式的關係)。
ECMAScript 5 的 Object.getOwnPropertyDescriptor()方法只能用於例項屬 性,要取得原型屬性的描述符,必須直接在原型物件上呼叫 Object.getOwnProperty- Descriptor()方法。
2. 原型與 in 操作符
有兩種方式使用 in 操作符:單獨使用和在 for-in 迴圈中使用。在單獨使用時,in 操作符會在通 過物件能夠訪問給定屬性時返回 true,無論該屬性存在於例項中還是原型中。
由於 in 操作符只要通過物件能夠訪問到屬性就返回 true,hasOwnProperty()只在屬性存在於 例項中時才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以確 定屬性是原型中的屬性。
3. 更簡單的原型語法
為了從視覺上更好地封裝原型的功能,更常見的做法是用一個包含所有屬性和方法的 物件字面量來重寫整個原型物件,如下面的例子所示。
function Person(){ }
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
在上面的程式碼中,我們將 Person.prototype 設定為等於一個以物件字面量形式建立的新物件。 終結果相同,但有一個例外:constructor 屬性不再指向 Person 了。前面曾經介紹過,每建立一 個函式,就會同時建立它的 prototype 物件,這個物件也會自動獲得 constructor 屬性。而我們在 這裡使用的語法,本質上完全重寫了預設的 prototype 物件,因此 constructor 屬性也就變成了新 物件的 constructor 屬性(指向 Object 建構函式),不再指向 Person 函式。此時,儘管 instanceof 操作符還能返回正確的結果,但通過 constructor 已經無法確定物件的型別了,如下所示
var friend = new Person();
alert(friend instanceof Object);
//true
alert(friend instanceof Person);
//true
alert(friend.constructor == Person);
//false
alert(friend.constructor == Object);
//true
在此,用 instanceof 操作符測試 Object 和 Person 仍然返回 true,但 constructor 屬性則 等於 Object 而不等於 Person 了。如果 constructor 的值真的很重要,可以像下面這樣特意將它設 置回適當的值。
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上程式碼特意包含了一個 constructor 屬性,並將它的值設定為 Person,從而確保了通過該屬 效能夠訪問到適當的值。
注意,以這種方式重設 constructor 屬性會導致它的[[Enumerable]]特性被設定為 true。預設 情況下,原生的 constructor 屬性是不可列舉的,因此如果你使用相容 ECMAScript 5的 JavaScript引 擎,可以試一試 Object.defineProperty()。
function Person(){ }
Person.prototype = {
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
//重設建構函式,只適用於 ECMAScript 5相容的瀏覽器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
4. 原型的動態性
5. 原生物件的原型
6. 原型物件的問題
組合使用建構函式模式和原型模式
建立自定義型別的常見方式,就是組合使用建構函式模式與原型模式。建構函式模式用於定義實 例屬性,而原型模式用於定義方法和共享的屬性。結果,每個例項都會有自己的一份例項屬性的副本, 但同時又共享著對方法的引用,大限度地節省了記憶體。另外,這種混成模式還支援向建構函式傳遞參 數;可謂是集兩種模式之長。下面的程式碼重寫了前面的例子。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van"); alert(person1.friends);
//"Shelby,Count,Van"
alert(person2.friends);
//"Shelby,Count"
alert(person1.friends === person2.friends);
//false
alert(person1.sayName === person2.sayName);
//true
在這個例子中,例項屬性都是在建構函式中定義的,而由所有例項共享的屬性 constructor 和方 法 sayName()則是在原型中定義的。而修改了 person1.friends(向其中新增一個新字串),並不 會影響到 person2.friends,因為它們分別引用了不同的陣列。
這種建構函式與原型混成的模式,是目前在 ECMAScript中使用廣泛、認同度高的一種建立自 定義型別的方法。可以說,這是用來定義引用型別的一種預設模式。
動態原型模式
有其他 OO語言經驗的開發人員在看到獨立的建構函式和原型時,很可能會感到非常困惑。動態原 型模式正是致力於解決這個問題的一個方案,它把所有資訊都封裝在了建構函式中,而通過在建構函式 中初始化原型(僅在必要的情況下),又保持了同時使用建構函式和原型的優點。換句話說,可以通過 檢查某個應該存在的方法是否有效,來決定是否需要初始化原型。來看一個例子。
function Person(name, age, job){
//屬性
this.name = name;
this.age = age;
this.job = job;
//方法
if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();
注意建構函式程式碼中加粗的部分。這裡只在 sayName()方法不存在的情況下,才會將它新增到原 型中。這段程式碼只會在初次呼叫建構函式時才會執行。此後,原型已經完成初始化,不需要再做什麼修 改了。不過要記住,這裡對原型所做的修改,能夠立即在所有例項中得到反映。因此,這種方法確實可 以說非常完美。其中,if 語句檢查的可以是初始化之後應該存在的任何屬性或方法——不必用一大堆 if 語句檢查每個屬性和每個方法;只要檢查其中一個即可。對於採用這種模式建立的物件,還可以使 用 instanceof 操作符確定它的型別。
使用動態原型模式時,不能使用物件字面量重寫原型。前面已經解釋過了,如果 在已經建立了例項的情況下重寫原型,那麼就會切斷現有例項與新原型之間的聯絡。
寄生建構函式模式
通常,在前述的幾種模式都不適用的情況下,可以使用寄生(parasitic)建構函式模式。這種模式 的基本思想是建立一個函式,該函式的作用僅僅是封裝建立物件的程式碼,然後再返回新建立的物件;但 從表面上看,這個函式又很像是典型的建構函式。下面是一個例子。
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
.在這個例子中,Person 函式建立了一個新物件,並以相應的屬性和方法初始化該物件,然後又返 回了這個物件。除了使用 new 操作符並把使用的包裝函式叫做建構函式之外,這個模式跟工廠模式其實 是一模一樣的。建構函式在不返回值的情況下,預設會返回新物件例項。而通過在建構函式的末尾新增一個 return 語句,可以重寫呼叫建構函式時返回的值。 這個模式可以在特殊的情況下用來為物件建立建構函式。假設我們想建立一個具有額外方法的特殊 陣列。由於不能直接修改 Array 建構函式,因此可以使用這個模式。
function SpecialArray(){
//建立陣列
var values = new Array();
//新增值
values.push.apply(values, arguments);
//新增方法
values.toPipedString = function(){
return this.join("|");
};
//返回陣列
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
在這個例子中,我們建立了一個名叫 SpecialArray 的建構函式。在這個函式內部,首先建立了 一個數組,然後 push()方法(用建構函式接收到的所有引數)初始化了陣列的值。隨後,又給陣列實 例添加了一個 toPipedString()方法,該方法返回以豎線分割的陣列值。後,將陣列以函式值的形 式返回。接著,我們呼叫了 SpecialArray 建構函式,向其中傳入了用於初始化陣列的值,此後又調 用了 toPipedString()方法。
關於寄生建構函式模式,有一點需要說明:首先,返回的物件與建構函式或者與建構函式的原型屬 性之間沒有關係;也就是說,建構函式返回的物件與在建構函式外部建立的物件沒有什麼不同。為此, 不能依賴 instanceof 操作符來確定物件型別。由於存在上述問題,我們建議在可以使用其他模式的情 況下,不要使用這種模式。
穩妥建構函式模式
道格拉斯·克羅克福德(Douglas Crockford)發明了 JavaScript中的穩妥物件(durable objects)這 個概念。所謂穩妥物件,指的是沒有公共屬性,而且其方法也不引用 this 的物件。穩妥物件適合在 一些安全的環境中(這些環境中會禁止使用 this 和 new),或者在防止資料被其他應用程式(如 Mashup 程式)改動時使用。穩妥建構函式遵循與寄生建構函式類似的模式,但有兩點不同:一是新建立物件的 例項方法不引用 this;二是不使用 new 操作符呼叫建構函式。按照穩妥建構函式的要求,可以將前面 的 Person 建構函式重寫如下。
function Person(name, age, job){
//建立要返回的物件
var o = new Object();
//可以在這裡定義私有變數和函式
//新增方法
o.sayName = function(){
alert(name);
};
//返回物件
return o;
}
注意,在以這種模式建立的物件中,除了使用 sayName()方法之外,沒有其他辦法訪問 name 的值。 可以像下面使用穩妥的 Person 建構函式。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
這樣,變數 friend 中儲存的是一個穩妥物件,而除了呼叫 sayName()方法外,沒有別的方式可 以訪問其資料成員。即使有其他程式碼會給這個物件新增方法或資料成員,但也不可能有別的辦法訪問傳 入到建構函式中的原始資料。穩妥建構函式模式提供的這種安全性,使得它非常適合在某些安全執行環 境——例如,ADsafe(www.adsafe.org)和 Caja(http://code.google.com/p/google-caja/)提供的環境—— 下使用
與寄生建構函式模式類似,使用穩妥建構函式模式建立的物件與建構函式之間也 沒有什麼關係,因此 instanceof 操作符對這種物件也沒有意義。
理解繼承
繼承是 OO語言中的一個為人津津樂道的概念。許多 OO語言都支援兩種繼承方式:介面繼承和 實現繼承。介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。如前所述,由於函式沒有簽名, 在 ECMAScript中無法實現介面繼承。ECMAScript只支援實現繼承,而且其實現繼承主要是依靠原型鏈 來實現的。
原型鏈
ECMAScript 中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法。
其基本思想是利用原 型讓一個引用型別繼承另一個引用型別的屬性和方法。簡單回顧一下建構函式、原型和例項的關係:每 個建構函式都有一個原型物件,原型物件都包含一個指向建構函式的指標,而例項都包含一個指向原型 物件的內部指標。那麼,假如我們讓原型物件等於另一個型別的例項,結果會怎麼樣呢?顯然,此時的 原型物件將包含一個指向另一個原型的指標,相應地,另一個原型中也包含著一個指向另一個建構函式 的指標。假如另一個原型又是另一個型別的例項,那麼上述關係依然成立,如此層層遞進,就構成了實 例與原型的鏈條。這就是所謂原型鏈的基本概念。
實現原型鏈有一種基本模式,其程式碼大致如下。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//繼承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
以上程式碼定義了兩個型別:SuperType 和 SubType。每個型別分別有一個屬性和一個方法。它們 的主要區別是 SubType 繼承了 SuperType,而繼承是通過建立 SuperType 的例項,並將該例項賦給 SubType.prototype 實現的。實現的本質是重寫原型物件,代之以一個新型別的例項。換句話說,原 來存在於 SuperType 的例項中的所有屬性和方法,現在也存在於 SubType.prototype 中了。在確立了 繼承關係之後,我們給 SubType.prototype 添加了一個方法,這樣就在繼承了 SuperType 的屬性和方 法的基礎上又添加了一個新方法。這個例子中的例項以及建構函式和原型之間的關係如圖6-4所示。
在上面的程式碼中,我們沒有使用 SubType 預設提供的原型,而是給它換了一個新原型;這個新原型 就是 SuperType 的例項。於是,新原型不僅具有作為一個 SuperType 的例項所擁有的全部屬性和方法, 而且其內部還有一個指標,指向了 SuperType 的原型。終結果就是這樣的:instance 指向 SubType 的原型,SubType 的原型又指向 SuperType 的原型。getSuperValue() 方法仍然還在 SuperType.prototype 中,但 property 則位於 SubType.prototype 中。這是因為 property 是一 個例項屬性,而 getSuperValue()則是一個原型方法。既然 SubType.prototype 現在是 SuperType的例項,那麼 property 當然就位於該例項中了。此外,要注意 instance.constructor 現在指向的 是 SuperType,這是因為原來 SubType.prototype 中的 constructor 被重寫了的緣故①。
① 實際上,不是 SubType 的原型的 constructor 屬性被重寫了,而是 SubType 的原型指向了另一個物件—— SuperType 的原型,而這個原型物件的 constructor 屬性指向的是 SuperType。
通過實現原型鏈,本質上擴充套件了本章前面介紹的原型搜尋機制。讀者大概還記得,當以讀取模式訪 問一個例項屬性時,首先會在例項中搜索該屬性。如果沒有找到該屬性,則會繼續搜尋例項的原型。在 通過原型鏈實現繼承的情況下,搜尋過程就得以沿著原型鏈繼續向上。就拿上面的例子來說,呼叫 instance.getSuperValue()會經歷三個搜尋步驟:1)搜尋例項;2)搜尋 SubType.prototype; 3)搜尋 SuperType.prototype,後一步才會找到該方法。在找不到屬性或方法的情況下,搜尋過 程總是要一環一環地前行到原型鏈末端才會停下來。
1. 別忘記預設的原型
事實上,前面例子中展示的原型鏈還少一環。我們知道,所有引用型別預設都繼承了 Object,而 這個繼承也是通過原型鏈實現的。大家要記住,所有函式的預設原型都是 Object 的例項,因此預設原 型都會包含一個內部指標,指向 Object.prototype。這也正是所有自定義型別都會繼承 toString()、 valueOf()等預設方法的根本原因。所以,我們說上面例子展示的原型鏈中還應該包括另外一個繼承層 次。圖 6-5為我們展示了該例子中完整的原型鏈。
一句話,SubType繼承了SuperType,而SuperType繼承了Object。當呼叫instance.toString() 時,實