相關文章: 面向對象的程序設計(一) — 建立對象 http://www.cnblogs.com/blackwood/archive/2013/04/24/3039523.htmlhtml
繼承是OO語言中的一個最爲人津津樂道的概念。 許多OO語言都支持兩種繼承方式:接口繼承和實現繼承。 接口繼承只繼承方法簽名,而實現繼承則繼承實際的方法。 如其所述,因爲函數沒有簽名,在ECMAScript中沒法實現接口繼承。 ECMAScript只支持實現繼承,並且其實現繼承主要是依靠原型鏈來實現的。編程
ECMAScript中描述了原型鏈的概念,並將原型鏈做爲實現繼承的主要方法。 其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。 簡單回顧一下構造函數、原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。 那麼,假如咱們讓原型對象等於另外一個類型的實例,結果會如何呢? 顯然,此時的原型對象將包含一個指向另外一個原型的指針,相應的,另外一個原型中也包含着一個指向另外一個構造函數的指針。 假如另外一個原型又是另外一個類型的實例,那麼上述關係依然成立,如此層層遞進,就 構成了實例與原型的鏈條。 這就是所謂原型鏈的基本概念 。
實現原型鏈有一種基本模式,其代碼大體以下:數組
function SuperType() { this.flag = true; } SuperType.prototype.getSuperFlag = function() { return this.flag; }; function SubType() { this.subFlag = false; } //繼承了SuperType SubType.prototype = new SuperType(); SubType.prototype.getSubFlag = function() { return this.subFlag; }; var st = new SubType(); alert(st.getSuperFlag()); //true alert(st instanceof Object); //true alert(st instanceof SuperType); //true alert(st instanceof SubType); //true alert(Object.prototype.isPrototypeOf(st)); //true alert(SuperType.prototype.isPrototypeOf(st)); //true alert(SubType.prototype.isPrototypeOf(st)); //true
以上代碼定義了兩個類型:SuperType和SubType。每一個類型分別有一個屬性和一個方法。 它們的主要區別是SbuType繼承了SuperType,而繼承是經過建立SuperType的實例,並將該實例賦給SubType.prototype實現的。 實現的本質是重寫原型對象,代之以一個新類型的實例。 換句話說,原來存在於SuperType的實例中的全部屬性和方法,如今也存在於SubType.prototype中了。在確立了繼承關係以後,咱們給SubType.prototype添加了一個方法,這樣就在繼承了SuperType的屬性和方法的基礎上又添加了一個新方法。markdown
在上面的代碼中,咱們沒有使用SubType默認提供的原型,而是給它換了一個新原型; 這個原型就是SuperType的實例。 因而新原型不只具備做爲一個SuperType實例所擁有的所有屬性和方法,而其內部還有一個指針,指向了SuperType的原型。 最終結果就是這樣的:st指向SubType.prototype,SubType.prototype又指向SuperType.prototype。 getSuperFlag()方法仍然還在SuperType.prototype中,但flag位於SubType.prototype中,這是由於flag是一個實例的屬性,而getSuperFlag()則是一個原型方法。 既然SubType.prototype如今是SuperType的實例,那麼flag固然就位於該實例中了。 此外要注意的是,st.constructor如今指向的是SuperType,這是由於原來的SubType.prototype.constructor被重寫了(SuperType實例的constructor—SuperType)。app
經過實現原型鏈,本質上擴展了前面(建立對象那裏)介紹的原型搜索機制。 當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性。若是沒找到該屬性,則會繼續搜索實例的原型。 在經過原型鏈實現繼承的狀況下,搜索過程就得以沿着原型鏈繼續向上。 就拿上面的例子來講,調用st.getSuperFlag()會經歷三個步驟:1)搜索實例;2)搜索SubType.prototype; 3)搜索SuperType.prototype,最後一步纔會找到該方法。 在找不到屬性或方法的狀況下,搜索過程老是要一環一環地前行到原型鏈末端纔會停下來。函數
事實上,前面例子中展現的原型鏈還少一環。 咱們知道,全部引用類型默認都繼承了Object,而這個繼承也是經過原型鏈實現的。你們要記住,全部函數的默認原型都是Object的實例。 所以默認原型都會包含一個內部指針,指向Object.prototype。 這也正是全部自定義類型都會繼承toString()、valueOf()等默認方法的根本緣由。 因此,咱們說上面的例子展現的原型鏈中還應包含另一個繼承層次。 SuperType.prototype裏會有一個指針指向Object.prototype。測試
一句話SubType繼承了SuperType,而SuperType繼承了Object。當調用st.toString()時,實際上用的是保存在Object.prototype中的那個方法。this
能夠經過兩種方法來肯定原型和實例之間的關係。 第一種方法是使用instanceof操做符,只要用這個操做符來測試實例與原型鏈中出現過的構造函數,結果就會返回ture。以下:spa
alert(st instanceof Object); //true alert(st instanceof SuperType); //true alert(st instanceof SubType); //true
因爲原型鏈的關係,咱們能夠說st是Object、SuperType、SubType中的任何一個類型的實例。 所以,測試這三個構造函數的結果都返回了true。prototype
第二種方法是使用isPrototypeOf()方法。 一樣,只要是原型鏈中出現過的原型,均可以說是該原型鏈所派生的實例的原型,所以isPrototypeOf()方法也會返回true,以下:
alert(Object.prototype.isPrototypeOf(st)); //true alert(SuperType.prototype.isPrototypeOf(st)); //true alert(SubType.prototype.isPrototypeOf(st)); //true
子類型有時須要重寫超類型中的某個方法,或者須要添加超類型中不存在的某個方法。 但無論怎樣,給原型添加方法的代碼必定要放在替換原型的語句以後。 例:
function SuperType() { this.flag = true; } SuperType.prototype.getSuperFlag = function() { return this.flag; }; function SubType() { this.subFlag = false; } //繼承SuperType SubType.prototype = new SuperType(); //添加新方法 SubType.prototype.getSubFlag = function() { return this.subFlag; }; //重寫超類型中的方法 SubType.prototype.getSuperFlag = function() { return false; }; var st = new SubType(); alert(st.getSuperFlag()); //false
在以上代碼中,SubType.prototype定義了兩個方法。第一個方法getSubFlag()方法被添加到了SubType中。第二個方法getSuperFlag()是原型鏈中已經存在的一個方法,但重寫這個方法將會屏蔽原來的那個方法。 換句話說,當經過SubType實例調用getSuperFlag()時,調用的就是這個從新定義的方法;但經過SuperType的實例調用getSuperFlag()時,還會繼續調用原來的那個方法。 格外要注意的是,必須在SuperType的實例替換SubType的原型以後,再定義這個方法。
在經過原型鏈實現繼承時,不能使用對象字面量建立原型。 由於這樣作就會重寫原型。 以下:
function SuperType() { this.flag = true; } SuperType.prototype.getSuperFlag = function() { alert(this.flag); }; function SubType() { this.subFlag = false; } //繼承SuperType SubType.prototype = new SuperType(); //使用對象字面量添加新方法,會致使上一行代碼無效 SubType.prototype = { getSubFlag: function(){ return this.subFlag; } }; var st = new SubType(); alert(st.getSuperFlag()); //error!
以上代碼展現了把SuperType的實例賦給原型以後,緊接着又將原型替換成一個對象字面量而致使的問題。 因爲如今的原型包含的是一個Object實例,而非SuperType實例,所以咱們設想中的原型鏈已經被切斷—SubType和SuperType之間已經沒有關係了。
原型鏈雖然很強大,能夠用它來實現繼承,但它也存在一些問題。 其中,最主要的問題來自包含引用類型值的原型。 前面介紹過(建立對象部分),包含引用類型值的原型屬性會被全部實例共享; 而這也正是爲何要在構造函數中,而不是原型中定義屬性的緣由。在經過原型來實現繼承時,原型實際上會變成另外一個類型的實例。 因而原先的實例屬性也就瓜熟蒂落的變成了如今的原型屬性。 例:
function SuperType() { this.colors = ["red", "blue", "green"]; } function SubType() {} SubType.prototype = new SuperType(); var st1 = new SubType(); st1.colors.push("black"); alert(st1.colors); //'red,blue,green,black' var st2 = new SubType(); alert(st2.colors); //'red,blue,green,balck'
這個例子中的SuperType構造函數定義了一個colors屬性,該屬性包含一個數組(引用類型值)。SuperType的每一個實例都會有各自包含本身數組的colors屬性。 當SubType經過原型鏈繼承了SuperType以後,SubType.prototype就變成了SuperType的一個實例,所以它也擁有了一個它本身的colors屬性—就跟專門建立了一個SubType.prototype.colors屬性同樣。 但結果是,SubType的全部實例都會共享這一個colors屬性。 而咱們對st1.colors的修改可以經過st2.colors反映出來,就已經充分證明了這一點。
原型鏈的第二問題是:在建立子類型的實例時,不能向超類型的構造函數中傳遞參數。 實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。 有鑑於此,再加上前面剛剛討論過的因爲原型中包含引用類型值所帶來的問題,實踐中不多會單獨使用原型鏈。
在解決原型中包含引用類型值所帶來的問題中,開發人員開始使用一種叫作借用構造函數的技術(有時候也叫作僞造對象或經典繼承)。 這種技術的基本思想至關簡單,即在子類型構造函數的內部調用超類型構造函數。 別忘了,函數只不過是在特定環境中執行代碼的對象,所以經過使用applay()和call()方法也能夠在(未來)新建立的對象上執行構造函數,以下所示:
function SuperType() { this.colors = ['red', 'blue', 'green']; } function SubType() { SuperType.call(this); //調用SuperType()構造函數,並將當前this傳入 } var st1 = new SubType(); st1.colors.push('black'); alert(st1.colors); //'red,blue,green,black' var st2 = new SubType(); alert(st2.colors); //'red,blue,green'
代碼中SubType()構造函數中「借調」了超類型的構造函數。 經過使用call()方法,咱們其實是在(將來將要)新建立的SubType實例的環境下調用了SuperType構造函數。 這樣一來,就會在新SubType對象上執行SuperType()函數中定義的全部對象初始化代碼。 結果,,SubType的而每一個實例都會具備本身的colors屬性副本了。
相對於原型鏈而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳遞參數。以下:
function SuperType(name) { this.name = name; } function SubType() { //繼承了SuperType,同時傳遞了參數 SuperType.call(this,'nico'); this.age = 22; } var st = new SubType(); alert(st.name); //'nico' alert(st.age); //22
爲了確保SuperType構造函數不會重寫子類型的屬性,能夠在調用超類型構造函數後,再添加應該在子類型中定義的屬性。
沒法避免的構造函數模式存在的問題—方法都在函數中定義,所以函數複用就無從談起了。 並且,在超類型的原型中定義的方法,對子類型而言也是不可見的。 結果全部類型都只能使用構造函數模式。 考慮到這些問題,借用構造函數技術也是不多單獨使用的。
組合繼承,有時候也叫作僞經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮兩者之長的一種繼承模式。 其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。 這樣,既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有它本身的屬性。 例:
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.sayAge = function() { alert(this.age); }; var st1 = new SubType('nico',22); st1.colors.push('black'); alert(st1.colors); //'red,blue,green,black' st1.sayName(); //'nico' st1.sayAge(); //'22' var st2 = new SubType('nana',23); alert(st2.colors); //'red,blue,green' st2.sayName(); //'nana' st2.sayAge(); //'23'
在這個例子中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()對傳入其中的對象執行了一次淺複製。 來看下面的例子:
function object(o) { function F () {} F.prototype = o; return new F(); } var person = { name: "nico", friends: ['nana','john'] }; var person1 = object(person); person1.name = 'Greg'; person.friends.push('rob'); var person2 = object(person); person2.name = 'Linda'; person2.friends.push('kobe'); alert(person.friends); //'nana,john,rob,kobe'
(在這以前,來小小複習一下關於參數傳遞的問題。 ECMAScript中全部的函數的參數都是按值傳遞的,在向參數傳遞引用類型的值時,會把這個值在內存中的地址複製給一個局部變量,所以這個局部變量的變化會反映在函數的外部。
雖然能夠經過對象實例訪問保存在原型中的值,卻不能經過實例重寫原型中的值。 若是在實例中添加了一個與實例原型同名的屬性,該屬性將屏蔽原型中的那個屬性, 當訪問該屬性的時候,先會在實例中搜索該屬性是否存在,它確實存在因此返回它的值,再也不搜索原型了。)
這種原型式繼承,要求必須有一個對象能夠做爲另外一個對象的基礎。若是有這麼一個對象的話,能夠把它傳遞給object()函數,而後再根據具體需求對獲得的對象加以修改便可。 在這個例子中,能夠做爲另外一個對象基礎的是person對象,因而咱們把它傳入到object()函數中,而後改函數會返回一個新對象。 這個新對象將person做爲原型,因此它的原型中就包含一個基本類型值屬性和一個引用類型屬性。 這意味着preson.friends不只屬於person全部,並且也會被person1和person2共享(指向同一個地址)。實際上,這就至關於又建立了person對象的兩個副本。
在沒有必要興師動衆地建立構造函數,而只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的。 不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣。
一樣是由克羅克福德推廣的
寄生式繼承是與原型式繼承緊密相關的一種思路。 寄生式繼承的思路與寄生構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。 例:
function object(o) { function F () {} F.prototype = o; return new F(); } 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: "nico", friends: ['nana', 'john'] }; 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.sayAge = function() { alert(this.age); };
在第一次調用SuperType()構造函數的時候,SubType.prototype會獲得兩個屬性name和colors,他們都是SuperType的實例屬性, 只不過如今位於SubType的原型中。 當調用SubType構造函數時,又會調用一次SuperType()構造函數,這一次又在新對象上建立了實例屬性name和colors。因而,這兩個屬性就屏蔽了原型中的兩個同名屬性。
有兩組name和colors屬性,一組在實例上,一組在SubType原型中。 這就是調用兩次SuperType構造函數的結果。好在咱們已經找到了解決這個問題的方法—寄生組合式繼承。
所謂寄生組和式繼承,即經過借用構造函數來繼承屬性, 經過原型鏈的混成形式來繼承方法。 其背後的基本思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數, 咱們所須要的無非就是超類型原型的一個副本而已。 本質上,就是使用寄生式繼承來繼承超類型的原型, 而後再將結果指定給子類型的原型。 寄生組和式繼承的基本模式以下所示:
function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; }
這個示例中的inheritPrototype()函數實現了寄生組合式繼承的最簡單形式。 這個函數接收兩個參數:子類型構造函數和超類型構造函數。 在函數內部,第一步是建立超類型原型的一個副本。 第二步是爲建立的副本添加constructor屬性,從而彌補因重寫原型而失去的默認的constructor屬性。 最後一步,將新建立的對象(即副本)賦值給子類型的原型。 這樣,咱們就能夠用調用inheritPrototype()函數的語句, 去替換前面例子中位子類型原型賦值的語句了。例如:
function inheritPrototype(subType, superType) { var prototype = object(superType.prototype); prototype.constructor = subType; subType.prototype = prototype; } function object(o) { function F () {} F.prototype = o; return new F(); } 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; } //替換了組合繼承模式中建立SuperType實例賦值給SubType.prototype inheritPrototype(SubType, SuperType); SubType.prototype.sayAge = function() { alert(this.age); };
這個例子的高效率提如今它只調用了一次SuperType構造函數,而且所以避免了在SubType.prototype上面建立沒必要要的、多餘的屬性。 於此同時,原型鏈還能保持不變; 所以,還可以正常使用instanceof和isPrototypeof()。 開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式。
ECMAScript支持面向對象(OO)編程, 但不使用類或接口。 對象能夠在代碼執行過程當中建立和加強, 所以具備動態性而非嚴格定義的實體。 在沒有類的狀況下, 能夠採用下列模式建立對象。
原型模式
JavaScript主要經過原型鏈實現繼承。 原型鏈的構建是經過講一個類型的實例賦值給另外一個構造函數的原型實現的。 這樣, 子類型就可以訪問超類型的全部屬性和方法。 使用最多的繼承模式是組合繼承, 這種模式使用原型鏈繼承共享的屬性和方法, 而經過構造函數繼承實例屬性。
此外,還存在下列可供選擇的繼承模型。
原型式繼承
「面向對象的程序設計」這一章是我看得最頭痛的一章,一開始的時候被原型啊構造函數啊繼承啊寄生啊什麼的繞暈掉了, 又是在紙上畫圖又是寫各類例子好不容啃完了第一遍, 隔了段時間又啃了第二遍, 如今終於算是所有能理解了, 心理頓時輕鬆好多 。
相關文章: 面向對象的程序設計(一) — 建立對象 http://www.cnblogs.com/blackwood/archive/2013/04/24/3039523.html