javaScript 基於原型的對象繼承

主題一、原型

一、基於原型的語言的特點

1 只有對象,沒有類;對象繼承對象,而不是類繼承類。

2  “原型對象”是基於原型語言的核心概念。原型對象是新對象的模板,它將自身的屬性共享給新對象。一個對象不但可以享有自己創建時和運行時定義的屬性,而且可以享有原型對象的屬性。

3 除了語言原生的頂級對象,每一個對象都有自己的原型對象,所有對象構成一個樹狀的層級系統。root節點的頂層對象是一個語言原生的對象,其他所有對象都直接或間接繼承它的屬性。

顯然,基於原型的語言比基於類的語言簡單得多,我們只需要知道"用對象去創建對象",就可以在原型的世界裏大行其道了!

二、基於原型的語言中對象的創建

創建有兩個步驟
1. 使用"原型對象"作爲"模板"生成新對象
這個步驟是必要的,這是每個對象出生的唯一方式。以原型爲模板創建對象,這也是"原型"(prototype)的原意。
2. 初始化內部屬性
這一步驟不是必要的。通俗點說,就是,對"複製品"不滿意,我們可以"再加工",使之獲得不同於"模板"的"個性"。
這兩個步驟很自然,也很好理解,比使用類構造對象從概念上簡單得多了。對於習慣了java基於類的面向對象的語言的程序員, 這種"新穎"的生成對象的方式一定會讓他們感到好奇。

三、原型,爲複用代碼而生

使用原型,能複用代碼,節省內存空間 

舉個例子,存在舊對象oldObject,它有一個屬性name,值是’Andy’, 和一個名爲getName()的方法,如果以該對象爲原型創建一個新對象,
Js代碼  收藏代碼
  1. newObject = create(oldObject);  
 
那麼新對象newObject同樣具有屬性name,值也是’Andy’,也有一個方法getName()。值得注意的是,newObject並不是在內存中克隆了oldObject,它只是引用了oldObject的屬性, 導致實際的效果好像"複製"了newObject一樣。

 newObject = create(oldObject);創建的對象newObject只有一個屬性,這個屬性的值是原型對象的地址(或者引用),如下圖所示。


當對象訪問屬性的時候,如果在內部找不到,那麼會在原型對象中查找到屬性;如果原型對象中仍然找不到屬性,原型對象會查找自身的原型對象,如此循環下去,直至找到屬性或者到達頂級對象。對象查找屬性的過程所經過的對象構成一條鏈條,稱之爲原型鏈。newObject,oldObject和topObject就構成一條原型鏈。

下面列出newObject的3種的查找屬性情況

newObject查找name,
1 內部找不到,到原型對象中查找
2 oldObject中查找到了name,成功返回;

newObject查找toString
1 內部找不到,到原型對象中查找
2 oldObject中查找不到toString,到原型對象中查找
3 topObject中查找到了toString,成功返回;

newObject查找valueOf
1 內部找不到,到原型對象中查找
2 oldObject中查找不到valueOf,到原型對象中查找
3 topObject中還是找不到,而且topObject是頂層對象,所以返回錯誤或者空值。

對象會通過原型鏈動態地查找屬性,對象的所擁有的屬性並不是靜態的。如果原型鏈上的一個對象發生的改變,那麼這個改變也會馬上會反應到在原型鏈中處於該對象下方的所有對象。


三、繼承
如果以oldObject爲原型創建了newObject,那麼可以說newObject繼承了oldObject。
在java中 通過語句class Cat extends Animal定義Cat類繼承Animal類,Cat類產生的實例對象便擁有了Animal類中定義的屬性。類似地,在基於原型的語言中, 通過cat = create(animal)創建以animal對象爲模板的cat對象,cat對象便擁有了animal對象中的屬性,因此可以說cat對象繼承了anmial對象。 

四、小結
原型的本質就是對象引用原型對象的屬性,實現代碼複用。
基於原型的語言是以原型對象爲模板創建對象newObject = create(oldObject)。



主題二、深刻理解JavaScript基於原型的面向對象

一、飽受爭議的javascript

javascript本質上是基於原型的語言,但是卻引入了基於類的語言的new關鍵字和constructor模式,導致javascript飽受爭議。

javascript的作者Brendan Eich 1994年研發這門語言的時候,C++語言是最流行的語言,java1.0即將發佈,面向對象編程勢不可擋,於是他認爲,引入new關鍵字可以使習慣C++/java程序員更容易接受和使用javascript。

實際上,事實證明引入new是個錯誤的決定。

C++/java程序員看到new一個 function的時候,他們會認爲js通過function創建對象,function相當於類,接着他們會嘗試在js挖掘類似java/C++面向類的編程特性,結果他們發現function沒有extends,反而有個很奇怪的prototype對象,於是他們開始咒罵,js的面向對象太糟糕了。確實,new的引入讓他們以爲js的面向對象與java/C++類似,實際上並不是,如果不是以原型本質去理解js的面向對象,註定要遭受挫折,new,prototype,__proto__都是javascript實現原型的具體手段。

另一方面,理解原型的程序員,也表示不高興,因爲居然要使用new function的語法來間接實現原型繼承,三行代碼才做到最基本的原型繼承,下面是實現對象newObject繼承對象oldObject的代碼,
Js代碼  收藏代碼
  1. function F(){};  
  2. F.prototype = oldObject;  
  3. var newObject = new F();  
這太繁瑣了。基於原型語言理論上應該存在一個函數create(prototypeObject),功能是基於原型對象產生新對象,例如,
var newObject = create(oldObject);
看到這樣的代碼,人們就會自然很清晰地聯想到,newObject是以oldObject模板構造出來的。

js是世界上最容易被誤解的語言,原因主要有兩個:

1) 作爲基於原型的語言中,卻連最基本的一個通過原型產生對象的函數create(prototypeObject)也沒有,讓人不知道js根本上是以對象創建對象。應該添加該函數,現在Chrome和IE9的Object對象就有這個create函數。

2) 使用new func形式創建對象,讓人誤會js是以類似java類的構造函數創建對象,實際上,構造函數根本上在創建對象上起到次要的作用,甚至不需要,重要的只有函數的屬性prototype引用的原型對象,新對象以此爲模板生成,生成之後才調用函數做初始化的操作,而初始化操作不是必要的。應該把廢棄new 操作符,把new func分解爲兩步操作,
var newObject = create(func.prototype); 
func.call(newObject);
這樣程序員纔好理解。如果想把這兩個步驟合二爲一,應該使用new以外的關鍵字。

到這裏,我們務必要牢牢印入腦海的是,js的面向對象是基於原型的面向對象,對象創建的方式根本上只有一種,就是以原型對象爲模板創建對象,newObject = create(oldObject)。new function不是通過函數創建對象,只是刻意模仿java的表象。

js在面向對象上遭遇的爭議,完全是因爲商業因素導致作者失去了自己的立場。就像現在什麼產品都加個雲一樣,如果那時候不加個new關鍵字來標榜自己面向對象,產生"js其實類似c++/java"的煙幕,可能根本沒有人去關注javascript。更令人啼笑皆非的是,原本稱作LiveScript的javascript,因爲 後期和SUN合作,並且爲了沾上當時被SUN炒得火熱的Java的光,發佈的時候居然改名成Javascript。

二、從原型本質,站在語言設計者角度,理解constructor模式
假想我們是當時設計javascript繼承機制的Brendan Eich,我們會怎麼設計js的面向對象呢?
現在javascript開發到這樣的階段
1) 擁有基本類型,分支和循環,基本的數學運算,
2) 所有數據都是對象
3) 擁有類似C語言的function
4) 可以用var obj = {}語句生成一個空對象,然後使用obj.xxx或obj[xxx]設置對象屬性
5) 沒有繼承,沒有this關鍵字,沒有new
我們任務是,實現javascript的面向對象,最好能達到類似java的創建對象和繼承效果。更具體一點,我們要擴充js語言,實現類似下面的java代碼。
Java代碼  收藏代碼
  1. class Empolyee{  
  2.     String name;  
  3.     public Employee(String name){  
  4.         this.name = name;  
  5.     }  
  6.     public getName(){  
  7.         return this.name;  
  8.     }  
  9. }  
  10. class Coder extends Employee {  
  11.     String language;  
  12.     public Coder(name,language){  
  13.         super(name);  
  14.         this.language = language;  
  15.     }  
  16.     public getLanguage(){  
  17.         return this.language;  
  18.     }  
  19. }  
 
1 實現創建對象
現有的對象都是基本類型,怎麼創建用戶自定義的對象呢?
(解釋:
var i = 1;
這裏的i是解釋器幫忙封裝的Number對象,雖然看起來跟C的int沒區別,但實際上可以i.toString()。
)
java使用構造函數來產生對象,我們嘗試把java的Empolyee的構造函數代碼拷貝下來,看看可不可以模仿
Js代碼  收藏代碼
  1. function Empolyee(name){  
  2.     this.name = name;  
  3. }  
 
我們只要生成一個空對象obj,再把函數裏面的this換成obj,執行函數,就可以生成自定義對象啦!我們把Employee這樣用來創建對象的函數稱作構造函數。
1) 首先我們用原生的方式爲function添加方法call和apply,實現把把函數裏面的this替換成obj。call,apply在Lisp語言中已經有實現,很好參考和實現。
2) 然後實現生成實例 
Js代碼  收藏代碼
  1. function Empolyee(name){  
  2.     this.name = name;  
  3. }  
  4. var employee = {};  
  5. Employee.call(employee,'Jack');  
 
    
3) 到這裏,以類似java方式產生對象基本完成了,但是這個employee對象沒有方法
我們的function是第一類對象,可以運行時創建,可以當做變量賦值,所以沒有問題
Js代碼  收藏代碼
  1. function Empolyee(name){  
  2.     this.name = name;  
  3.     this.getName = function(){return this.name};  
  4. }  
 
2 實現繼承
創建對象成功了,接着考慮實現繼承。現在我們所有數據都是對象,沒有類,有兩種方案擺在我們的面前
a.類繼承
b.原型繼承
2.a實現類繼承
a方案是首選方案,因爲跟java相似的話,JS更容易被接受
先粘貼Java構造函數的代碼
Js代碼  收藏代碼
  1. function Coder extends Employee(name,language){  
  2.     super(name);  
  3.     this.language = language;  
  4. }  
 
1) 把extends後面的函數自動記錄下來,放到function對象的parentFunc變量
2) 如果第一行是super(),替換成var parent = newInstance(Coder.parentFunc,XXX),這樣內部保留一個名爲parent父對象;
3) 把this替換爲obj,super替換換成parent
4) "."和"[]"重新定義,需要支持在對象內部parent對象查找屬性。
這四步都屬於比較大的改動,只要認真想一想都覺得不是太容易。
更重要的是,即使把這4步實現了,不但語言變得太複雜了,而且產生的對象根本享受不了繼承帶來的好處——內存中的代碼複用,因爲這樣產生的每個對象都有"父類(函數)"的代碼而不是僅有一份。這時候該注意到java中使用類的意義了,java類的代碼在內存只有一份,然後每個對象執行方法都是引用類的代碼,所有子類對象調用父類方法的時候,執行的代碼都是同一份父類的方法代碼。但是JS沒有類,屬性和方法都是存在對象之中,根本沒有辦法做到java那樣通過類把代碼共享給所有對象!
a方案宣告失敗
2.b 實現原型繼承
看b方案。我們現在的js語言,一切都是對象,顯然非常適合使用基於原型的繼承方式,就看具體如何實現了。
我們新建一個topObject來代表頂層對象,那麼創建employee對象的時候,應該在employee對象內部設置一個屬性引用topObject;同理,創建coder對象的時候,應該在coder對象內部設置一個屬性引用employee對象,我們把這個引用原型對象的屬性命名約定爲"__proto__"。更進一步,爲了構建一個對象的過程更自然,構建時候應該先在新對象中設置引用原型對象的屬性,以表示先用模板製作出一個和模板一致的對象,然後再才執行構造函數初始化這個新對象自身的屬性,以添加個性化的東西。具體實現代碼如下:
Js代碼  收藏代碼
  1. var topObject = {  
  2.     __version__ : 1.0;  
  3. };  
  4.   
  5. function Empolyee(name){  
  6.     this.name = name;  
  7.     this.getName = function(){return this.name};  
  8. }  
  9. var employee = {};  
  10. employee.__proto__ = topObject;  
  11. Employee.call(employee,'Jack');  
  12.   
  13. function Coder(name,language){  
  14.     this.name = name;  
  15.     this.language = this.language;  
  16.     this.getLanguage = function(){return this.language};  
  17. }  
  18.   
  19. var coder = {};  
  20. coder.__proto__ = employee;  
  21. Coder.call(coder,'Coder Jack','Java');  
 
當然我們還要做的工作就是在javascript解釋器中增加對__proto__的支持,當一個對象訪問一個自身沒有的屬性的時候,就通過__proto__屬性查找原型鏈上是否存在該屬性。
優化1. 函數封裝
這一切看起來並不是那麼美好,我們創建一個employee對象需要3行代碼,我們需要這麼一個函數封裝這3行代碼
function newInstance(prototype,constructor,arg1,arg2,....);
//第一個參數是原型對象,第二個是構造函數,後面的是構造函數的參數
可以這麼實現
Js代碼  收藏代碼
  1. function sliceArguments(argumentsObj,n){  
  2.     var args = [];  
  3.     for(var i=0;i<argumentsObj.length;i++){  
  4.         if(i>=n){  
  5.             args.push(argumentsObj[i]);  
  6.         }  
  7.     }  
  8. }  
  9. function newInstance(prototype,constructor){  
  10.     var obj = {};  
  11.     obj.__proto__ = prototype;  
  12.     constructor.apply(obj,sliceArguments(arguments,2));  
  13. }  
  14. var employee = newInstance(topObject,Employee,'Jack');  
  15. var coder = newInstance(employee,Coder,'Coder Jack','Java');  
 
優化2. 縮減參數
仔細一看,function newInstance的參數可以更少,我們可以把原型對象prototype作爲屬性放在constructor,那樣我們的函數就可以只有一個參數了。屬性名就約定爲prototype吧。
2.1 我們修改解釋器,把topObject寫入語言作爲原生的頂級對象;再修改function的源代碼,讓每一個新建的function都默認具有屬性prototype = topObject
2.2 優化後的代碼如下
Js代碼  收藏代碼
  1. function newInstance(constructor){  
  2.     var obj = {};  
  3.     obj.__proto__ = constructor.prototype;  
  4.     constructor.apply(obj,sliceArguments(arguments,1));  
  5.     return obj;  
  6. }  
  7. function Employee(name){  
  8.     this.name = name;  
  9.     this.getName = function(){return this.name};  
  10. }  
  11. var employee = newInstance(Empolyee,'Jack');  
  12. var employee2 = newInstance(Empolyee,'Jack2');  
  13. var employee3 = newInstance(Empolyee,'Jack3');  
  14. function Coder(name,language){  
  15.     this.name = name;  
  16.     this.language = language;  
  17.     this.getLanguage = function(){return this.language};  
  18. }  
  19. Coder.prototype = newInstance(Empolyee,'');  
  20.   
  21. var coder = newInstance(Coder,'Coder Jack','Java');  
  22. var coder2 = newInstance(Coder,'Coder Lee','C#');  
  23. var coder3 = newInstance(Coder,'Coder Liu','C++');  
  24. var coder4 = newInstance(Coder,'Coder Liu','JavaScript');  
 
到達這一步,可以發現,我們的最終實現和Breandan Eich非常類似,在期待儘量模仿java創建對象的前提下,Brendan Eich 當時的設計是合乎情理的,是良好的。他相對於我們方案的唯一不同就是他使用了new關鍵字,而我們使用了newInstance函數。
儘管new關鍵字容易讓人誤解,但是背後偉大的思想,決定了時至今日,javascript依然是瀏覽器編程語言的龍頭大哥,甚至發展到複雜的node.js服務端編程。
三、從javascript的原型本質,理解javascript的構造器模式
在"從原型本質,站在語言設計者角度,理解constructor模式"一節中我們站在設計者角度粗略重現了js的設計過程。現在我們換個角色,不是語言設計者,而是熟悉原型概念並且知道js是基於原型的語言的程序員,去理解js的使用(new關鍵字+函數)的創建對象方式。
1. 理解new func()
Js代碼  收藏代碼
  1. function Employee(name){  
  2.     this.name = name;  
  3.     this.getName = function(){return this.name};  
  4. }  
  5. var employee = new Employee('Jack');  
 
分析上面代碼。
javascript引入new關鍵字是爲了模仿java創建對象的方式,通過語句var employee = new Employee('Jack') 就生成了一個employee對象。
我們知道,基於原型的語言生成一個步驟有兩步,第一步是使用"原型對象"作爲"模板"生成新對象,第二步是初始化新對象的內部屬性。
我們敢肯定地推斷,javascript中的new Employee('Jack');必然做了這兩件事情,那麼
1 "原型對象"在哪裏?
2 怎麼做到"初始化新對象的內部屬性"?
答案是,Employee.prototype就是我們要找的"原型對象",通過"以新對象代替this,執行Employee函數"做到了"初始化新對象的內部屬性"。
使用new+function的方式創建對象,其實就是應用我們設計的函數newInstance時的思想
Js代碼  收藏代碼
  1. function newInstance(constructor){  
  2.         var obj = {};  
  3.         obj.__proto__ = constructor.prototype;  
  4.         constructor.call(obj,sliceArguments(arguments,1));  
  5.         return obj;  
  6. }  
 
javascript把生成一個對象所需的兩個元素——"原型對象"和"初始化"都集中在構造函數,以簡化創建對象的過程,其實是個良好的設計。唯一的缺點是new關鍵字容易讓人誤會。
2. 簡單羅列javascript構造器模式的特點
1) javascript的頂層對象是Object.prototype
2) 所有對象有一個__proto__屬性。__proto__指向自己的"原型對象",搜索屬性的原型鏈以__proto__爲基礎。
3) 每個函數都會默認關聯一個原型對象。javascript每創建一個函數的時候,都同時創建一個原型對象,賦值到函數的prototype屬性,用作使用new  生成實例對象的默認原型對象。該默認原型對象的內容是
Js代碼  收藏代碼
  1. {  
  2.     __proto__:Object.prototype,  
  3.     constructor: 指向函數本身  
  4. }  
 
__proto__指向Object.prototype的目的是爲了使生成的實例對象繼承頂層對象Object.prototype;
而constructor指向函數本身的目的是爲了使生成的實例對象newObject可以直接通過newObject.constructor訪問到構造函數,同時構造函數和原型對象可以互相訪問也是個良好的設計。但是,實際上,constructor並沒有什麼用,所以大家可以不理會這個屬性,這僅僅是一個優化的設計。
 
構造函數,原型對象,實例對象的三角關係圖如下


 
4) 可以修改或替換構造函數都會默認關聯的原型對象。需要注意的的是,不少資料說,如果是使用自定義的對象替換了構造函數f默認關聯的原型對象,最好添加一行代碼
Js代碼  收藏代碼
  1. f.prototype.constructor=f  
 
   以確保維護正確的三角關係。
   例如 
Js代碼  收藏代碼
  1. function Employee(){};  
  2. function Coder(){};  
  3. Coder.prototype = new Employee();  
  4. Coder.prototype.constructor = Coder;  
  5. var coder = new Coder();  
 
   但是經過我的測試,即使不寫上一行Coder.prototype.constructor = Coder;,以下測試都表現正確
Js代碼  收藏代碼
  1. coder instanceOf Coder//true  
  2. Coder.prototype.isPrototypeOf(coder)//true  
 
                   也就是說原型對象的construtctor屬性根本不影響繼承,它只是普通的一個附加屬性,沒有任何特殊作用,我們可以完全無視這個屬性。
   不寫上一行Coder.prototype.constructor = Coder;,唯一會引起的錯誤只有,coder.constructor的結果是Employee,而不是Coder。實
   際上我們並不會關心coder.constructor,我們關心的只是是繼承,所以即使不寫上一行Coder.prototype.constructor = Coder;也沒有關係。
5) 以下代碼幾乎涵蓋了上面所討論的特點,建議讀者在chrome中運行該代碼以加深對構造器模式的理解
   5.a.代碼
Php代碼  收藏代碼
  1. <script>  
  2.     function Employee(name){  
  3.         this.name = name;  
  4.         //this.getName = function(){return this.name};方法代碼應該放到原型對象之中,而不是初始化函數中,這樣每個employee對象都共享同一個方法代碼  
  5.     }  
  6.     Employee.prototype.getName = function(){return this.name};  
  7.     var employee = new Employee('Jack');  
  8.     console.log("employee.getName(): " + employee.getName());//Jack  
  9.     var employee2 = new Employee('Jack2');  
  10.     console.log("employee2.getName(): " + employee2.getName());//Jack2    
  11.     function Coder(name,language){  
  12.         this.name = name;  
  13.         this.language = language;  
  14.         //this.getLanguage = function(){return this.language}; 方法代碼應該放到原型對象之中,而不是初始化函數中,這樣才能實現代碼共享  
  15.     }  
  16.     Coder.prototype = new Employee('');  
  17.     Coder.prototype.constructor = Coder;//這一句話其實也可以不寫,不影響繼承  
  18.     Coder.prototype.getLanguage = function(){return this.language};  
  19.   
  20.     var coder = new Coder('Coder Jack','Java');  
  21.     console.log("coder.getName(): " + coder.getName());//Coder Jack  
  22.     console.log("coder.getLanguage(): "+coder.getLanguage());//Java  
  23.     var coder2 = new Coder('Coder Lee','C#');  
  24.     console.log("coder2.getName(): " + coder2.getName());//Coder Lee  
  25.     console.log("coder2.getLanguage(): " + coder2.getLanguage());//C#  
  26.     var coder3 = new Coder('Coder Liu','C++');  
  27.     console.log("coder3.getLanguage(): " + coder3.getName());//Coder Liu  
  28.     console.log("coder3.getLanguage()" + coder3.getLanguage());//C++  
  29.       
  30.     console.log("employee.constructor: " + employee.constructor);  
  31.     console.log("employee.constructor.prototype === Employee.prototype: " + (employee.constructor.prototype === Employee.prototype));  
  32.     console.log("employee.constructor.prototype.constructor === Employee: " + (employee.constructor.prototype.constructor === Employee));  
  33.     console.log("employee instanceof Object: "  + (employee instanceof Object));  
  34.     console.log("employee instanceof Function: "  + (employee instanceof Function));  
  35.     console.log("employee instanceof Employee: "  + (employee instanceof Employee ));  
  36.     console.log("Employee.prototype.isPrototypeOf(employee): "  + (Employee.prototype.isPrototypeOf(employee)));  
  37.     console.log("Function.prototype.isPrototypeOf(employee): "  + (Function.prototype.isPrototypeOf(employee)));  
  38.     console.log("Object.prototype.isPrototypeOf(employee): "  + (Object.prototype.isPrototypeOf(employee)));  
  39.     console.log("coder.constructor: " + coder.constructor);  
  40.     console.log("coder instanceof Object: "  + (coder instanceof Object));  
  41.     console.log("coder instanceof Function: "  + (coder instanceof Function));  
  42.     console.log("coder instanceof Employee: "  + (coder instanceof Employee ));  
  43.     console.log("coder instanceof Coder: "  + (coder instanceof Coder ));  
  44.     console.log("Employee.prototype.isPrototypeOf(coder): "  + (Employee.prototype.isPrototypeOf(coder)));  
  45.     console.log("Coder.prototype.isPrototypeOf(coder): "  + (Coder.prototype.isPrototypeOf(coder)));  
  46.     console.log("Function.prototype.isPrototypeOf(coder): "  + (Function.prototype.isPrototypeOf(coder)));  
  47.     console.log("Object.prototype.isPrototypeOf(coder): "  + (Object.prototype.isPrototypeOf(coder)));  
  48.   </script>  
 
  
  5.b.對象繼承體系結構圖
  
  下圖是上面5.a代碼的對象整體結構圖(圖片較大,可以下載到本地縮小來看)
  從整體上看,這像極了java的類繼承體系結構,實際上這就是js的對象繼承體系結構。
  裏面的對象有三種角色,紫色的是構造函數,黃色的是原型對象,綠色的是實例對象,當然不能嚴格區分這些角色,例如匿名的Employee實例對象充當了Coder的原型對象。
  紫色的構造函數和原型對象之間有一個雙向箭頭,這個雙向箭頭的意思,構造函數有一個prototype屬性指向原型對象,而原型隊形也有一個constructor屬性指向構造函數,它們之間有着互相引用的關係。
  
  單線箭頭,表示的是對象繼承關係。
  


 

  從這個圖,我們可以直觀地看到
  1) 所有對象都有自己的原型對象。所有構造函數的原型對象都是Function.prototype,Object.prototype是最頂層的對象。我們可以在Function.prototype上增加方法,那麼在原型鏈下方的函數,就可獲得這些方法,同理我們可以在Object.prototype上增加方法,那麼js所有對象都擁有了這個方法。
  2) 通過原型繼承,所有對象構成了一個完整的系統
  3) 我相信你能夠發現更多有趣的的東西.如果你覺得這篇文章不值得一看,那麼請至少看看這張圖片,結合這張圖片重新思考下js原型的理念,應該能給你一些有益的回報。

6. 構造器模式的best practice  
  1) 方法最好放在原型對象中,讓每個實例對象都共享同一個方法。如果方法放在構造函數中,那麼每個對象都有自己獨立的一份方法代碼,浪費內存。
  2) 字段變量(fields,variables)最好放在構造函數中,讓每個實例對象都具有一份自己的字段。除非要在所有子類中共享,實現類似靜態變量的效果,才把字段放在原型中。
  3) 繼承層次不宜過深,原型鏈查找會耗費時間。
  例如,
  上面第5)點中的代碼片段中,
  1)Employee和Coder的方法都放在了原型中
  2)Coder產生的實例對象雖然繼承自匿名employee對象(new Employee('')),擁有name屬性,但是爲了每個Coder產生的實例對象都擁有屬於自己的一份name屬性,我們選擇在構造函數中重複定義name屬性,覆蓋匿名employee對象的name屬性。
  
四、模擬基於類的面向對象

1. 該不該模擬類
javascript是基於原型的語言,具有強大的表達能力,足可以模擬基於類的面向對象。相信大家也看過不少模擬類的js代碼,這裏不打算羅列。

但是,js畢竟是原型繼承的語言,應該要按照原型繼承的思維去表達面向對象,而不是用類的思維,這樣才能表現出js的真正的威力。
如果要模擬的話,模擬一些最基本的操作就可以,不要嘗試深入模擬基於類的語言的複雜特性,否則會犯下跟Brendan Eich同樣的錯誤。模擬的出發點是方便程序員能夠更容易地使用js面向對象,但是理解了構造器模式和原型鏈的前提下,沒有模擬的必要,只需要封裝一些常用的操作就OK了。
js的對象沒有類型,根本不需要像java的對象那樣需要關心自己的類繼承體系以檢查類型轉換是否正確,所以模仿類繼承沒有意義。在js中只需要關心對象的內容,關心對象能否繼承其他對象的屬性就足夠了。

我就曾經是一個被誤導的程序員。看別人寫的面向對象教程,以爲js需要我們開發一些函數,才能使用面向對象。我用過prototype.js 的Class.create,那時候我的感覺很不爽,我抱怨js爲什麼連最基本的class都沒有。

如果讓我寫一篇文章,介紹js的面向對象,我會先教會讀者領會這個函數,
Js代碼  收藏代碼
  1. if (typeof Object.create !== 'function') {  
  2.     Object.create = function (o) {  
  3.         function F() {}  
  4.         F.prototype = o;  
  5.         return new F();  
  6.     };  
  7. }  
  8. var newObject = Object.create(oldObject);  
 
我要讓讀者知道,js是基於原型的語言,它用只能以對象爲模板創建對象,它用對象繼承對象。它沒有類,也不需要類,一切都是對象。在這之後再介紹如何模擬class就無所謂了,因爲理解了javascript的原型本質之後,就會知道模擬類的實質是還是調用原型的特性,也就不會過分期待js能夠像java一樣操作類和對象,而且能夠發現原型的面向對象能夠帶來傳統面嚮對象語言無法比擬強大特性。

2. 欣賞Crockford對類的模擬
拜讀了Crockford的一些文章和他寫的JavaScript:The Good Parts,覺得他寫的一些對js的簡單封裝很有意思,也很實用。
下面是我對他封裝js面向對象的理解和總結,希望對讀者有用。

創建對象的方法根本上只有一種方式:以原型對象爲模板創建對象,但是在形式上可以多種多樣。
在JS中,從形式上,除去字面量方式創建對象之外,有三種常單創建對象的形式(or you can call it a "pattern",anyway)

1) 使用構造函數創建對象(constructor pattern)
Crockford通過函數Function.prototype.method和Function.prototype.inherits方法"美化"了傳統構造模式創建對象的代碼。
值得注意的是,method方法和inherits方法的封裝把prototype從代碼中除去,掩蓋了原型本質,需要程序理解構造器模式的前提下才可使用。
Js代碼  收藏代碼
  1. <script>  
  2. /** 
  3.  * 以原型對象爲模板創建出新對象 
  4.  * 這個函數已經被Chrome和IE9採用,所以需要有個判斷這個函數是否已經存在,Crockford的影響力可見一斑 
  5.  */  
  6. if(!Object.create){  
  7.     Object.create = function(oldObject){  
  8.         function F(){};  
  9.         F.prototype = oldObject;  
  10.         return new F();  
  11.     }  
  12. }  
  13. /** 
  14.  * 在構造函數的原型對象上添加方法 
  15.  * 非常推薦這個函數,因爲這個函數能夠培養出在原型對象中定義方法的良好習慣 
  16.  */  
  17. Function.prototype.method = function(name,func){  
  18.     if(!this.prototype[name]){  
  19.         this.prototype[name] = func;  
  20.         return this;  
  21.     }  
  22. };  
  23. /** 
  24.  * 使構造函數“繼承”其他構造函數 
  25.  * 實際上是將構造函數的原型對象替換爲另外構造函數產生的對象 
  26.  *  
  27.  */  
  28. Function.method('inherits',function(F){  
  29.     this.prototype = new F();  
  30.     return this;  
  31. });  
  32.   
  33. /***************************************** 
  34.  *使用鏈式代碼清晰緊湊地定義構造函數 
  35.  *****************************************/  
  36. var Employee = function(name){  
  37.     this.name = name;  
  38. }.method('getName',function(){  
  39.     return this.name;  
  40. });  
  41.   
  42. //由於method和inherits函數都返回this,所以可以非常舒服地將構造函數寫成鏈式代碼  
  43. var employee = new Employee("jack");  
  44. alert(employee.getName());  
  45.   
  46. //由於method和inherits函數都返回this,所以可以非常舒服地將構造函數寫成鏈式代碼  
  47. var Coder = function(name,language){  
  48.     this.name = name;  
  49.     this.language = language;  
  50. }.inherits(Employee)  
  51.  .method('getLanguage',function(){  
  52.     return this.language;  
  53.  })  
  54.  .method('getIntroduction',function(){  
  55.     return this.name + " is skilled in " + this.language;  
  56.  });  
  57.   
  58.   var coder = new Coder('Jack','Java');  
  59.   alert(coder.getIntroduction());  
  60.   alert(coder.getName());  
  61.   
  62.  </script>  
 
  
  增強1.模擬私有變量。
上面構造函數所產生的對象只有public成員,沒有private成員,可以通過閉包實現私有成員
Js代碼  收藏代碼
  1.     /***************************************** 
  2.  * 模擬私有變量 
  3.  *****************************************/  
  4. var Employee = function(name){  
  5.     //私有變量  
  6.     var name = name;              
  7.     this.getName = function(){return name};  
  8. };  
  9. var employee = new Employee('Jack');  
  10. alert(employee.name);//undefined  
  11. alert(employee.getName());//Jack  
 
私有成員帶來的代價是,訪問私有變量的方法不能放置在原型對象中被共享,導致每個生成的對象在內存都獨立擁有一份訪問私有變量方法的代碼。

  增前2. 模擬super.method.

構造函數coder能不能模仿出java中類似super.method的效果呢?答案是可以的,最簡單的實現是在對象內部創建一個副對象的副本。缺點是增加了內存的消耗。
Js代碼  收藏代碼
  1. /****************** 
  2.          *模擬super.method() 
  3.          ******************/  
  4. var Coder = function(name,language){  
  5.     var employee = new Employee('');  
  6.     //父類的getName方法  
  7.     var superGetName = employee.getName;  
  8.     this.name = name;  
  9.     this.language = language;  
  10.     this.getName = function(){  
  11.         return "my name is :" + superGetName.call(this,name);  
  12.     };  
  13. }.inherits(Employee)  
  14.  .method('getLanguage',function(){  
  15.     return this.language;  
  16.  })  
  17.  .method('getIntroduction',function(){  
  18.     return this.name + " is skilled in " + this.language;  
  19.  });  
  20.   
  21.   var coder = new Coder('Jack','Java');  
  22.   alert(coder.getIntroduction());  
  23.   alert(coder.getName());//my name is Jack  
 
2) 使用原型創建對象(prototypal pattern)

這種創建方式直白地顯示了原型語言創建對象的特點

Js代碼  收藏代碼
  1. <script>  
  2. /** 
  3.  * 以原型對象爲模板創建出新對象 
  4.  */  
  5. if(!Object.create){  
  6.     Object.create = function(oldObject){  
  7.         function F(){};  
  8.         F.prototype = oldObject;  
  9.         return new F();  
  10.     }  
  11. }  
  12.   
  13. /***************************************** 
  14.  * 使用原型對象創建對象,創建之後再對象初始化, 
  15.  * 這種創建方式直白地顯示了原型語言創建對象的特點 
  16.  *****************************************/  
  17.   
  18. var employee = {  
  19.     name: 'Jack',  
  20.     getName: function(){return this.name;}  
  21. };  
  22.   
  23. var coder = Object.create(employee);  
  24. coder.name = 'Jackson';  
  25. coder.language = 'language';  
  26. coder.getLanguage = 'Java';  
  27. coder.getIntroduction = function(){  
  28.     return this.name + " is skilled in " + this.language;  
  29. }  
  30. alert(coder.getName());  
  31. alert(coder.getIntroduction());  
  32.  </script>  
 

3) 使用函數創建對象並返回(functional pattern)
這種方式很簡單,在函數內部先新建一個對象 var object = {},然後爲這個對象設置屬性,最後返回這個對象
優點:a.最簡單最容易理解,甚至不需要理解js的原型特性,應該作爲最優先考慮的對象創建方式; 
  b.生成對象可以有私有屬性
  c.具有類似java中super.method()的訪問"父對象"方法的能力
缺點:同一函數生成的多個對象不能在內存中共享代碼。

Js代碼  收藏代碼
  1. <script>  
  2.         /** 
  3.      * 以原型對象爲模板創建出新對象 
  4.      * 這個函數已經被Chrome和IE9採用,所以需要有個判斷這個函數是否已經存在,Crockford的影響力可見一斑 
  5.      */  
  6.     if(!Object.create){  
  7.         Object.create = function(oldObject){  
  8.             function F(){};  
  9.             F.prototype = oldObject;  
  10.             return new F();  
  11.         }  
  12.     }  
  13.     /** 
  14.      * 在構造函數的原型對象上添加方法 
  15.      * 非常推薦這個函數,因爲這個函數能夠培養出在原型對象中定義方法的良好習慣 
  16.      */  
  17.     Function.prototype.method = function(name,func){  
  18.         if(!this.prototype[name]){  
  19.             this.prototype[name] = func;  
  20.             return this;  
  21.         }  
  22.     };  
  23.     /** 
  24.      * 使構造函數“繼承”其他構造函數 
  25.      * 實際上是將構造函數的原型對象替換爲另外構造函數產生的對象 
  26.      *  
  27.      */  
  28.     Function.method('inherits',function(F){  
  29.         this.prototype = new F();  
  30.         return this;  
  31.     });  
  32.       
  33.     /** 
  34.      * 創建父對象方法的副本 
  35.      */  
  36.     Object.method('superior',function(methodName){  
  37.         var that  =  this;  
  38.         var method = this[methodName];  
  39.         return function(){  
  40.             return method.apply(that,arguments);  
  41.         };  
  42.     });  
  43.     /***************************************** 
  44.      * 使用函數創建對象 
  45.      * 1 使用函數的閉包實現私有屬性 
  46.      * 2 子對象可以調用父對象的方法 
  47.      *****************************************/  
  48.     function employee(name){  
  49.         var object = {};  
  50.         //name屬性是私有變量  
  51.         var name = name;  
  52.         //定義一個getName私有變量的目的是,如果其他方法想調用getName方法,它們可以直接調用getName而不是object.getName。  
  53.         //如果該object.getName被外部篡改了,那麼其他引用var getName的方法並不會收到影響,這樣程序的健壯性有保證  
  54.         var getName = function(){  
  55.             return name;  
  56.         }  
  57.         //getName對外公開  
  58.         object.getName = getName;  
  59.         return object;  
  60.     }  
  61.   
  62.     function coder(name,language){  
  63.         var object = employee(name);  
  64.         //獲取父對象getName函數的副本  
  65.         var superGetName = object.superior('getName');  
  66.         var language = language;  
  67.         var getLanguage = function(){return language;};  
  68.         //調用父對象的方法  
  69.         var getName = function(){  
  70.             return  "my name is " + superGetName(name);  
  71.         };  
  72.   
  73.         object.getName = getName;  
  74.   
  75.         return object;  
  76.     }  
  77.     var e1 = employee('Jack');  
  78.     alert(e1.name);//undefined  
  79.     alert(e1.getName());//Jack  
  80.       
  81.     var c1 = coder('Jackson','Java');  
  82.     alert(c1.getName());//My name is Jack  
  83.       
  84.   </script>  
  附錄
推薦一些極好的關於JS面向對象的文章(每一篇都嚴重推薦,尤其是crockford和他的《JavaScript: The Good Parts》)

crockford大師

MDN

微軟雜誌

MSDN

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