前端重新學習(0)JavaScript-理解 面向對象的程序設計

什麼是面向對象

http://shem.xin/article-5

本文結構

 

創建對象

對象的繼承(有些不是很理解-後續會更新)

 


官方定義:對象是擁有屬性和方法的數據。

對象和函數比較: 函數是用來實現具體功能的代碼,用一種方式把他們組織起來,就是函數了。
                                   對象是有屬性和方法的一個東西,在對象中的函數就變成了方法。

細節比較
    
    對象同樣是對js代碼封裝,不過對象可以封裝函數(方法)。

    比如把某一類的函數(方法)都封裝到某個對象中。
    這樣可以系統的管理調用函數(方法)。

  •  比如我寫了很多的函數,只要知道我想要調用的函數是哪一類的。
  • 聲明相應的對象,就可以很容易的找到我要調用的函數(方法)。
  • 對象中的屬性就是變量,對象中的方法就是函數

對象在 JavaScript 中被稱爲引用類型的值,而且有一些內置的引用類型可以用來創建特定的對象,

現簡要總結如下: 

引用類型與傳統面向對象程序設計中的類相似,但實現不同;

  • Object 是一個基礎類型,其他所有類型都從 Object 繼承了基本的行爲;
  • Array 類型是一組值的有序列表,同時還提供了操作和轉換這些值的功能;
  • Date 類型提供了有關日期和時間的信息,包括當前日期和時間以及相關的計算功能;
  • RegExp 類型是 ECMAScript支持正則表達式的一個接口,提供了基本的和一些高級的正則表 達式功能。

函數實際上是 Function 類型的實例,因此函數也是對象;而這一點正是 JavaScript有特色的地 方。由於函數是對象,所以函數也擁有方法,可以用來增強其行爲。

因爲有了基本包裝類型,所以 JavaScript 中的基本類型值可以被當作對象來訪問。三種基本包裝類 型分別是:Boolean、Number 和 String。以下是它們共同的特徵:

  • 每個包裝類型都映射到同名的基本類型;
  • 在讀取模式下訪問基本類型值時,就會創建對應的基本包裝類型的一個對象,從而方便了數據 操作;
  • 操作基本類型值的語句一經執行完畢,就會立即銷燬新創建的包裝對象。

在所有代碼執行之前,作用域中就已經存在兩個內置對象:Global 和 Math。在大多數ECMAScript 實現中都不能直接訪問 Global 對象;不過,Web 瀏覽器實現了承擔該角色的 window 對象。全局變 量和函數都是 Global 對象的屬性。Math 對象提供了很多屬性和方法,用於輔助完成複雜的數學計算 任務。 
 

ECMAScript支持面向對象(OO)編程,但不使用類或者接口。對象可以在代碼執行過程中創建和 增強,因此具有動態性而非嚴格定義的實體。在沒有類的情況下,可以採用下列模式創建對象。 

  1. 工廠模式,使用簡單的函數創建對象,爲對象添加屬性和方法,然後返回對象。這個模式後來 被構造函數模式所取代。

  2. 構造函數模式,可以創建自定義引用類型,可以像創建內置對象實例一樣使用 new 操作符。不 過,構造函數模式也有缺點,即它的每個成員都無法得到複用,包括函數。由於函數可以不局 限於任何對象(即與對象具有鬆散耦合的特點),因此沒有理由不在多個對象間共享函數。

  3. 原型模式,使用構造函數的 prototype 屬性來指定那些應該共享的屬性和方法。組合使用構造 函數模式和原型模式時,使用構造函數定義實例屬性,而使用原型定義共享的屬性和方法。 

JavaScript 主要通過原型鏈實現繼承。原型鏈的構建是通過將一個類型的實例賦值給另一個構造函 數的原型實現的。這樣,子類型就能夠訪問超類型的所有屬性和方法,這一點與基於類的繼承很相似。 原型鏈的問題是對象實例共享所有繼承的屬性和方法,因此不適宜單獨使用。解決這個問題的技術是借 用構造函數,即在子類型構造函數的內部調用超類型構造函數。這樣就可以做到每個實例都具有自己的 屬性,同時還能保證只使用構造函數模式來定義類型。使用多的繼承模式是組合繼承,這種模式使用 原型鏈繼承共享的屬性和方法,而通過借用構造函數繼承實例屬性。 

此外,還存在下列可供選擇的繼承模式。 

  1. 原型式繼承,可以在不必預先定義構造函數的情況下實現繼承,其本質是執行對給定對象的淺 複製。而複製得到的副本還可以得到進一步改造。

  2. 寄生式繼承,與原型式繼承非常相似,也是基於某個對象或某些信息創建一個對象,然後增強 對象,後返回對象。爲了解決組合繼承模式由於多次調用超類型構造函數而導致的低效率問 題,可以將這個模式與組合繼承一起使用。

  3. 寄生組合式繼承,集寄生式繼承和組合繼承的優點與一身,是實現基於類型繼承的有效方式。

 

 

面向對象(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 操作符確定它的類型。 


使用動態原型模式時,不能使用對象字面量重寫原型。前面已經解釋過了,如果 在已經創建了實例的情況下重寫原型,那麼就會切斷現有實例與新原型之間的聯繫。
 

寄生構造函數模式 (理解爲工廠模式與構造函數的混搭(裏外都有New))

通常,在前述的幾種模式都不適用的情況下,可以使用寄生(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 操作符來確定對象類型。由於存在上述問題,我們建議在可以使用其他模式的情 況下,不要使用這種模式。 

穩妥構造函數模式 (沒new沒this的構造函數模式就是穩妥構造函數模式)

道格拉斯·克羅克福德(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 的實例,因此默認原 型都會包含一個內部指針,指向 Object.prototype。這也正是所有自定義類型都會繼承 toString()、 valueOf()等默認方法的根本原因。所以,我們說上面例子展示的原型鏈中還應該包括另外一個繼承層 次。圖 6-5爲我們展示了該例子中完整的原型鏈。 

一句話,SubType繼承了SuperType,而SuperType繼承了Object。當調用instance.toString() 時,實際上調用的是保存在 Object.prototype 中的那個方法。 

2. 確定原型和實例的關係 

可以通過兩種方式來確定原型和實例之間的關係。第一種方式是使用 instanceof 操作符,只要用 這個操作符來測試實例與原型鏈中出現過的構造函數,結果就會返回 true。以下幾行代碼就說明了這 一點。 

alert(instance instanceof Object);         //true
 alert(instance instanceof SuperType);      //true
 alert(instance instanceof SubType);         //true 

由於原型鏈的關係,我們可以說 instance 是 Object、SuperType 或 SubType 中任何一個類型 的實例。因此,測試這三個構造函數的結果都返回了 true。

第二種方式是使用 isPrototypeOf()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該 原型鏈所派生的實例的原型,因此 isPrototypeOf()方法也會返回 true,如下所示。 

alert(Object.prototype.isPrototypeOf(instance));         //true 
alert(SuperType.prototype.isPrototypeOf(instance));      //true 
alert(SubType.prototype.isPrototypeOf(instance));        //true 

3. 謹慎地定義方法 

子類型有時候需要重寫超類型中的某個方法,或者需要添加超類型中不存在的某個方法。但不管怎 樣,給原型添加方法的代碼一定要放在替換原型的語句之後。來看下面的例子。 

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; 
}; 
 
//重寫超類型中的方法
 SubType.prototype.getSuperValue = function (){ 
    return false; 
}; 
 
var instance = new SubType(); 
alert(instance.getSuperValue());   //false 

在以上代碼中,加粗的部分是兩個方法的定義。第一個方法 getSubValue()被添加到了 SubType 中。第二個方法 getSuperValue()是原型鏈中已經存在的一個方法,但重寫這個方法將會屏蔽原來的 那個方法。換句話說,當通過 SubType 的實例調用 getSuperValue()時,調用的就是這個重新定義 的方法;但通過 SuperType 的實例調用 getSuperValue()時,還會繼續調用原來的那個方法。這裏 要格外注意的是,必須在用 SuperType 的實例替換原型之後,再定義這兩個方法。

還有一點需要提醒讀者,即在通過原型鏈實現繼承時,不能使用對象字面量創建原型方法。因爲這 樣做就會重寫原型鏈,如下面的例子所示。 

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;
     }, 
 
    someOtherMethod : function (){
         return false; 
    }
 }; 
 
var instance = new SubType(); 
alert(instance.getSuperValue());   //error! 

以上代碼展示了剛剛把 SuperType 的實例賦值給原型,緊接着又將原型替換成一個對象字面量而 導致的問題。由於現在的原型包含的是一個 Object 的實例,而非 SuperType 的實例,因此我們設想 中的原型鏈已經被切斷——SubType 和 SuperType 之間已經沒有關係了。 

4. 原型鏈的問題 

原型鏈雖然很強大,可以用它來實現繼承,但它也存在一些問題。其中,主要的問題來自包含引 用類型值的原型。想必大家還記得,我們前面介紹過包含引用類型值的原型屬性會被所有實例共享;而 這也正是爲什麼要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原 型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了。 下列代碼可以用來說明這個問題。

function SuperType(){
     this.colors = ["red", "blue", "green"]; 
} 
 
function SubType(){
             } 
 
//繼承了 SuperType
 SubType.prototype = new SuperType(); 
 
var instance1 = new SubType();
 instance1.colors.push("black");
 alert(instance1.colors);
    //"red,blue,green,black" 
 
var instance2 = new SubType();
 alert(instance2.colors);      //"red,blue,green,black" 

這個例子中的 SuperType 構造函數定義了一個 colors 屬性,該屬性包含一個數組(引用類型值)。 SuperType 的每個實例都會有各自包含自己數組的 colors 屬性。當 SubType 通過原型鏈繼承了 SuperType 之後,SubType.prototype 就變成了 SuperType 的一個實例,因此它也擁有了一個它自 己的 colors 屬性——就跟專門創建了一個 SubType.prototype.colors 屬性一樣。

但結果是什麼 呢?結果是 SubType 的所有實例都會共享這一個 colors 屬性。而我們對 instance1.colors 的修改 能夠通過 instance2.colors 反映出來,就已經充分證實了這一點。 原型鏈的第二個問題是:在創建子類型的實例時,不能向超類型的構造函數中傳遞參數。實際上, 應該說是沒有辦法在不影響所有對象實例的情況下,給超類型的構造函數傳遞參數。

有鑑於此,再加上 前面剛剛討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈。 

 

借用構造函數

在解決原型中包含引用類型值所帶來問題的過程中,開發人員開始使用一種叫做借用構造函數 (constructor stealing)的技術(有時候也叫做僞造對象或經典繼承)。這種技術的基本思想相當簡單,即 在子類型構造函數的內部調用超類型構造函數。別忘了,函數只不過是在特定環境中執行代碼的對象, 因此通過使用 apply()和 call()方法也可以在(將來)新創建的對象上執行構造函數,如下所示: 

function SuperType(){
     this.colors = ["red", "blue", "green"];
 } 
 
function SubType(){ 
      //繼承了 SuperType
     SuperType.call(this);
 } 
 
var instance1 = new SubType();
 instance1.colors.push("black"); 
alert(instance1.colors);    //"red,blue,green,black" 
 
var instance2 = new SubType();
 alert(instance2.colors);    //"red,blue,green" 

代碼中加粗的那一行代碼“借調”了超類型的構造函數。通過使用 call()方法(或 apply()方法 也可以),我們實際上是在(未來將要)新創建的 SubType 實例的環境下調用了 SuperType 構造函數。 這樣一來,就會在新 SubType 對象上執行 SuperType()函數中定義的所有對象初始化代碼。結果, SubType 的每個實例就都會具有自己的 colors 屬性的副本了。

1. 傳遞參數 

相對於原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函 數傳遞參數。看下面這個例子。 
 

function SuperType(name){ 
    this.name = name;
 } 
 
function SubType(){    
   //繼承了 SuperType,同時還傳遞了參數
     SuperType.call(this, "Nicholas"); 
         //實例屬性    
     this.age = 29; 
} 
 
var instance = new SubType();
 alert(instance.name); 
   //"Nicholas";
 alert(instance.age);     //29 
 

以上代碼中的 SuperType 只接受一個參數 name,該參數會直接賦給一個屬性。在 SubType 構造 函數內部調用 SuperType 構造函數時,實際上是爲 SubType 的實例設置了 name 屬性。爲了確保 SuperType 構造函數不會重寫子類型的屬性,可以在調用超類型構造函數後,再添加應該在子類型中 定義的屬性。

2. 借用構造函數的問題

如果僅僅是借用構造函數,那麼也將無法避免構造函數模式存在的問題——方法都在構造函數中定 義,因此函數複用就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結 果所有類型都只能使用構造函數模式。考慮到這些問題,借用構造函數的技術也是很少單獨使用的。 

組合繼承

組合繼承(combination inheritance),有時候也叫做僞經典繼承,指的是將原型鏈和借用構造函數的 技術組合到一塊,從而發揮二者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方 法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數 複用,又能夠保證每個實例都有它自己的屬性。下面來看一個例子。 


 function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
 } 
 
SuperType.prototype.sayName = function(){
     alert(this.name);
}; 
 
function SubType(name, age){ 
  
 
    //繼承屬性
     SuperType.call(this, name);
          this.age = age;
 } 
 
//繼承方法
 SubType.prototype = new SuperType(); 
SubType.prototype.constructor = SubType; 
SubType.prototype.sayAge = function(){
     alert(this.age); 
}; 
 
var instance1 = new SubType("Nicholas", 29);
 instance1.colors.push("black"); 
alert(instance1.colors);      //"red,blue,green,black" 
instance1.sayName();          //"Nicholas";
 instance1.sayAge();           //29 
 
var instance2 = new SubType("Greg", 27); 
alert(instance2.colors);      //"red,blue,green"
 instance2.sayName();          //"Greg"; 
instance2.sayAge();           //27 

在這個例子中,SuperType 構造函數定義了兩個屬性:name 和 colors。SuperType 的原型定義 了一個方法 sayName()。SubType 構造函數在調用 SuperType 構造函數時傳入了 name 參數,緊接着 又定義了它自己的屬性 age。然後,將 SuperType 的實例賦值給 SubType 的原型,然後又在該新原型 上定義了方法 sayAge()。

這樣一來,就可以讓兩個不同的 SubType 實例既分別擁有自己屬性——包 括 colors 屬性,又可以使用相同的方法了。 組合繼承避免了原型鏈和借用構造函數的缺陷,融合了它們的優點,成爲 JavaScript中常用的繼 承模式。而且,instanceof 和 isPrototypeOf()也能夠用於識別基於組合繼承創建的對象。 

 

 

原型式繼承

道格拉斯·克羅克福德在 2006年寫了一篇文章,題爲 Prototypal Inheritance in JavaScript (JavaScript 中的原型式繼承)。在這篇文章中,他介紹了一種實現繼承的方法,這種方法並沒有使用嚴格意義上的 構造函數。他的想法是藉助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。爲 了達到這個目的,他給出瞭如下函數。 

function object(o){
     function F(){} 
    F.prototype = o;
     return new F();
 } 
 

在 object()函數內部,先創建了一個臨時性的構造函數,然後將傳入的對象作爲這個構造函數的 原型,後返回了這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺複製。來看下面的例子。 

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"] }; 
 
var anotherPerson = object(person);
 anotherPerson.name = "Greg";
 anotherPerson.friends.push("Rob"); 
 
var yetAnotherPerson = object(person);
 yetAnotherPerson.name = "Linda";
 yetAnotherPerson.friends.push("Barbie"); 
 
alert(person.friends);   //"Shelby,Court,Van,Rob,Barbie" 

克羅克福德主張的這種原型式繼承,要求你必須有一個對象可以作爲另一個對象的基礎。如果有這麼 一個對象的話,可以把它傳遞給 object()函數,然後再根據具體需求對得到的對象加以修改即可。在這 個例子中,可以作爲另一個對象基礎的是 person 對象,於是我們把它傳入到 object()函數中,然後該 函數就會返回一個新對象。

這個新對象將 person 作爲原型,所以它的原型中就包含一個基本類型值屬性 和一個引用類型值屬性。這意味着 person.friends 不僅屬於 person 所有,而且也會被 anotherPerson 以及 yetAnotherPerson 共享。實際上,這就相當於又創建了 person 對象的兩個副本。 ECMAScript 5通過新增 Object.create()方法規範化了原型式繼承。這個方法接收兩個參數:一 個用作新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的情況下, Object.create()與 object()方法的行爲相同。 
 

var person = { 
    name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
 }; 
 
var anotherPerson = Object.create(person);
 anotherPerson.name = "Greg";
 anotherPerson.friends.push("Rob");
      var yetAnotherPerson = Object.create(person);
 
yetAnotherPerson.name = "Linda";
 yetAnotherPerson.friends.push("Barbie"); 
 
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 

Object.create()方法的第二個參數與Object.defineProperties()方法的第二個參數格式相 同:每個屬性都是通過自己的描述符定義的。以這種方式指定的任何屬性都會覆蓋原型對象上的同名屬 性。例如: 

var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"]
 }; 
var anotherPerson = Object.create(person, {
     name: {         value: "Greg"     }
 }); 
     alert(anotherPerson.name); //"Greg" 

支持 Object.create()方法的瀏覽器有 IE9+、Firefox 4+、Safari 5+、Opera 12+和 Chrome。

在沒有必要興師動衆地創建構造函數,而只想讓一個對象與另一個對象保持類似的情況下,原型式 繼承是完全可以勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模 式一樣。

 

寄生式繼承

寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路,並且同樣也是由克羅克福德推而廣 之的。寄生式繼承的思路與寄生構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該 函數在內部以某種方式來增強對象,後再像真地是它做了所有工作一樣返回對象。以下代碼示範了寄 生式繼承模式。 

.

function createAnother(original){
     var clone = object(original);  //通過調用函數創建一個新對象
     clone.sayHi = function(){
      //以某種方式來增強這個對象
         alert("hi");
     };
     return clone;
         //返回這個對象
 }

在這個例子中,createAnother()函數接收了一個參數,也就是將要作爲新對象基礎的對象。然 後,把這個對象(original)傳遞給 object()函數,將返回的結果賦值給 clone。再爲 clone 對象 添加一個新方法 sayHi(),後返回 clone 對象。可以像下面這樣來使用 createAnother()函數: 


 var person = {
     name: "Nicholas",
     friends: ["Shelby", "Court", "Van"] }; 
 
var anotherPerson = createAnother(person);
 anotherPerson.sayHi(); //"hi" 
 

這個例子中的代碼基於 person 返回了一個新對象——anotherPerson。新對象不僅具有 person 的所有屬性和方法,而且還有自己的 sayHi()方法。 在主要考慮對象而不是自定義類型和構造函數的情況下,寄生式繼承也是一種有用的模式。前面示 範繼承模式時使用的 object()函數不是必需的;任何能夠返回新對象的函數都適用於此模式。 

使用寄生式繼承來爲對象添加函數,會由於不能做到函數複用而降低效率;這一 點與構造函數模式類似。 

 

 

 

 

寄生組合式繼承

 

前面說過,組合繼承是 JavaScript 常用的繼承模式;不過,它也有自己的不足。組合繼承大的 問題就是無論什麼情況下,都會調用兩次超類型構造函數:一次是在創建子類型原型的時候,另一次是 在子類型構造函數內部。沒錯,子類型終會包含超類型對象的全部實例屬性,但我們不得不在調用子 類型構造函數時重寫這些屬性。再來看一看下面組合繼承的例子。 

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
 } 
 
SuperType.prototype.sayName = function(){ 
    alert(this.name);
 }; 
 
function SubType(name, age){ 
      SuperType.call(this, name); 
        //第二次調用 SuperType() 
         this.age = age;
 } 
 
SubType.prototype = new SuperType();
    //第一次調用 SuperType() 
SubType.prototype.constructor = SubType;
 SubType.prototype.sayAge = function(){ 
    alert(this.age); 
}; 

加粗字體的行中是調用 SuperType 構造函數的代碼。在第一次調用 SuperType 構造函數時, SubType.prototype 會得到兩個屬性:name 和 colors;它們都是 SuperType 的實例屬性,只不過 現在位於 SubType 的原型中。當調用 SubType 構造函數時,又會調用一次 SuperType 構造函數,這 一次又在新對象上創建了實例屬性 name 和 colors。於是,這兩個屬性就屏蔽了原型中的兩個同名屬 性。

圖 6-6展示了上述過程。 如圖 6-6所示,有兩組 name 和 colors 屬性:一組在實例上,一組在 SubType 原型中。這就是調 用兩次 SuperType 構造函數的結果。好在我們已經找到了解決這個問題方法——寄生組合式繼承。 所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。其背 後的基本思路是:不必爲了指定子類型的原型而調用超類型的構造函數,我們所需要的無非就是超類型 原型的一個副本而已。

本質上,就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型 的原型。寄生組合式繼承的基本模式如下所示。 

function inheritPrototype(subType, superType){
     var prototype = object(superType.prototype);     //創建對象
     prototype.constructor = subType;              //增強對象
     subType.prototype = prototype;               //指定對象
 } 

這個示例中的 inheritPrototype()函數實現了寄生組合式繼承的簡單形式。這個函數接收兩 個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是創建超類型原型的一個副本。第二 步是爲創建的副本添加 constructor 屬性,從而彌補因重寫原型而失去的默認的 constructor 屬性。 後一步,將新創建的對象(即副本)賦值給子類型的原型。這樣,我們就可以用調用 inherit- Prototype()函數的語句,去替換前面例子中爲子類型原型賦值的語句了,例如: 

function SuperType(name){
     this.name = name;
     this.colors = ["red", "blue", "green"];
 } 
 
SuperType.prototype.sayName = function(){
     alert(this.name);
 }; 
 
function SubType(name, age){
       SuperType.call(this, name);
          this.age = age;
 } 
 
inheritPrototype(SubType, SuperType); 
 
SubType.prototype.sayAge = function(){ 
    alert(this.age); 
}; 
 

這個例子的高效率體現在它只調用了一次 SuperType 構造函數,並且因此避免了在 SubType. prototype 上面創建不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用 instanceof 和 isPrototypeOf()。開發人員普遍認爲寄生組合式繼承是引用類型理想的繼承範式。
 

YUI的 YAHOO.lang.extend()方法採用了寄生組合繼承,從而讓這種模式首次 出現在了一個應用非常廣泛的 JavaScript庫中。要了解有關 YUI的更多信息,請訪問 http://developer. yahoo.com/yui/。 

.

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章