寫在前面的話:這篇博客不適合對面向對象一無所知的人,若是你連_proto_、prototype...都不是很瞭解的話,建議仍是先去了解一下JavaScript面向對象的基礎知識,畢竟胖子不是一口吃成的。博文有點長,若是能仔細看懂每一句話(畢竟都是《高程3》的原話),收穫不容小覷。有關面向對象的基礎知識,請參見:JS的從理解對象到建立對象.javascript
咱們都知道面嚮對象語言的三大特徵:繼承、封裝、多態,但JavaScript不是真正的面向對象,它只是基於面向對象,因此會有本身獨特的地方。這裏就說說JavaScript的繼承是如何實現的。html
學習過Java和c++的都知道,它們的繼承經過類實現,但JavaScript沒有類這個概念,那它經過什麼機制實現繼承呢? 答案是: 原型鏈! 其基本思想是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。
這篇博客主要是關於《高程3》—— 6.3 繼承 的總結,建議先閱讀阮一峯大神的js繼承三部曲,而後再回頭看體會更深:java
JavaScript面向對象編程(二):構造函數的繼承 編程
JavaScript面向對象編程(二):非構造函數的繼承 瀏覽器
下面這個是關於Function和Object建立實例之間的關係,不妨先了解一下它們之間複雜的關係:app
圖 1ide
實現繼承以前,先看一個基於原型鏈繼承的鏈圖,對繼承有個具體化的概念: (這個是核心繼承部分) 函數
圖 3學習
-------------------------------有了上面的思路,下面來看js中6種經典的實現繼承方法-----------------------------------------
導讀提示:方法1-3爲一個體系,方法4-6爲另外一個體系.
一、原型鏈繼承
簡單回顧一下構造函數、原型和實例的關係:每一個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針(constructor),而實例都包含一個指向原型對象的內部指針([[Prototype]])。 那麼,假如咱們讓原型對象(Prototype)等於另外一個類型的實例,結果會是怎樣?顯然,此時的原型對象將包含一個指向另外一個原型的指針,相應地,另外一個原型也包含着一個指向另外一個構造函數的指針,假如另外一個原型又是另外一個類型的實例,那麼上述關係依然成立,如此層層推動,就構成了實例與原型的鏈條。這就是所謂的原型鏈的基本概念,可能有些繞口,下面結合代碼理解。
1 function SuperType () { 2 this.property = true; 3 } 4 5 SuperType.prototype.getSuperValue = function() { 6 return this.property; 7 }; 8 9 function SubType() { 10 this.subproperty = false; 11 } 12 13 //繼承SuperType 14 SubType.prototype = new SuperType(); 15 16 SubType.prototype.getSubValue = function() { 17 return this.subproperty; 18 } 19 20 var instance =new SubType(); 21 console.log(instance.getSuperValue()); // true
直觀的模型圖請參見圖 2,同時,全部繼承都離不開Object() 這個終極Boss,所以,完整的原型鏈的原點就是Object對象,參見圖 3.
1.1 肯定原型和實例的關係:
實例沿着原型鏈向上查詢,只要是本身繼承的,都被認做本身的構造函數,測試以下
1 var instance =new SubType(); 2 console.log(instance instanceof Object); //true 3 console.log(instance instanceof SuperType); //true 4 console.log(instance instanceof SubType); //true 5 6 var instance1 =new SuperType(); 7 console.log(instance1 instanceof Object); //true 8 console.log(instance1 instanceof SuperType); //true 9 console.log(instance1 instanceof SubType); //false
1.2 謹慎定義方法:
子類型又時須要重寫超類型中的某個方法,或者須要添加超類型中不存在的某個方法。但無論怎麼樣,給原型添加方法的代碼必定要放在替換原型的語句以後。看正確代碼:
1 function SuperType() { 2 this.property = true; 3 } 4 5 SuperType.prototype.getSuperValue = function() { 6 return this.property; 7 }; 8 9 function SubType() { 10 this.subproperty = false; 11 } 12 13 //繼承SuperType 14 SubType.prototype = new SuperType(); 15 16 //添加新方法 ###必須放在上一句繼承SuperType以後,不然調用這個方法時會報錯--沒定義 17 SubType.prototype.getSubValue = function() { 18 return this.subproperty; 19 }; 20 21 //重寫超類中的方法 ###必須放在上一句繼承SuperType以後,不然調用這個方法時重寫失效 22 SubType.prototype.getSuperValue = function() { 23 return false; 24 } 25 26 var instance = new SubType(); 27 console.log(instance.getSuperValue()); //false 28 console.log(instance.getSubValue()); //false
!!!注意:經過原型鏈實現繼承時,不能使用對象字面量形式建立原型方法。由於那樣會重寫原型鏈,舉個栗子:
1 function SuperType() { 2 this.property = true; 3 } 4 5 SuperType.prototype.getSuperValue = function() { 6 return this.property; 7 }; 8 9 function SubType() { 10 this.subproperty = false; 11 } 12 13 //繼承SuperType 14 SubType.prototype = new SuperType(); 15 16 //使用字面量添加新方法,會致使上一行代碼無效 17 SubType.prototype ={ 18 getSubValue : function() { 19 return this.subproperty; 20 }, 21 22 someOtherMethod : funtion (){ 23 return false; 24 } 25 }; 26 27 var instance = new SubType(); 28 console.log(instance.getSuperValue()); // error!
以上代碼展現了剛剛把SuperType的實例賦值給原型,緊接着又將原型替換成一個對象字面量而致使的問題。因爲如今的原型包含的是一個Object的實例,而非SuperType的實例,所以咱們設想中的原型鏈已經被切斷——SubType和SuperType之間已經沒有關係了,即繼承語句SubType.prototype = new SuperType() 失效
1.3 原型鏈繼承的問題
第一個問題來自包含引用類型值的原型,由於它有這麼一個特性:包含引用類型值的原型屬性會被全部實例共享(修改),而在構造函數中的基本類型和引用類型屬性均不可改變(const附體);這也是爲何要在構造函數中,而不是原型對象中定義屬性的緣由。這裏經過原型來實現繼承,原型實際上會變成另外一個類型的實例。因而,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了,進而會被全部子類實例共享(修改)。補充一句:雖然能夠經過實例訪問保存在原型中的值,但卻不能經過對象實例重寫原型中的值。
1 function SuperType() { 2 this.colors = ["red","blue","green"]; //構造函數屬性(實例屬性),會被實例共享,但不會被修改 3 } 4 5 function SubType() { 6 } 7 8 //繼承了SuperType 9 SubType.prototype = new SuperType(); /*原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性*/ 10 11 var instance1 = new SubType();
/* instance1.colors = ["red","blue","green","black"]; 這種方式是給instance1新添加的屬性,覆蓋了原型colors,而不是修改了原型colors. */ 12 instance1.colors.push("black"); 13 console.log(instance1.colors); //"red,blue,green,black" 14 15 var instance2 = new SubType(); 16 console.log(instance2.colors); //"red,blue,green,black" Super中的實例屬性也變成能夠被改寫的,不理想
第二個問題就是:在建立子類型的實例時,不能向超類的構造函數中傳遞傳遞參數。實際上,應該說是沒有辦法在不影響全部對象實例的狀況下,給超類的構造函數傳遞參數。有鑑於此,再加上前面剛剛討論過的因爲原型中包含引用類型值所帶來的問題,實踐中不多會單獨使用原型鏈繼承。
二、借用構造函數
爲了解決原型鏈繼承帶來的問題,一種新的繼承應運而生——借用構造函數,其基本思想很簡單:在子類型構造函數的內部調用超類型。別忘了,函數只不過是在特定環境中執行的對象,所以經過使用apply()、call()方法也能夠在(未來)新建立的對象上執行構造函數,代碼以下:
1 function SuperType() { 2 this.colors = ["red","blue","green"]; 3 } 4 5 function SubType() { 6 //繼承了SuperType --從新建立SuperType構造函數屬性的副本 7 SuperType.call(this); 8 } 9 10 var instance1 = newe SubType(); 11 instance1.colors.push("black"); 12 console.log(instance1.colors); //"red,blue,green,black" 13 14 var instances2 = new SubType(); 15 console.log(instance2.colors); //"red,blue,green" --完美實現了繼承構造函數屬性
代碼中加粗那一行「借調」了超類的構造函數。經過使用call()方法(或apply()方法),咱們其實是在(將來將要)新建立的SubType實例的環境下調用了SuperType構造函數。這樣一來,就會在新的SubType對象上執行SuperType()函數中定義的全部對象初始化代碼。結果,SubType的每一個實例就都會具備本身的colors屬性副本了。
2.1 傳遞參數
相對於原型鏈繼承而言,借用構造函數有一個很大的優點,便可以在子類型構造函數中向超類型構造函數傳參。看下面這個例子
1 function SuperType() { 2 this.name = name; 3 } 4 5 function SubType() { 6 //繼承SuperType,同時還傳遞了參數 --從新建立SuperType構造函數屬性的副本 7 SuperType.call(this,"Nicholas"); 8 9 //實例屬性 10 this.age = 23; 11 } 12 13 var instance = new SubType(); 14 console.log(instance.name); // "Nicholas" 15 console.log(instance.age); // 23
注意:爲了保證子類構造函數屬性不會被超類重寫,可在調用超類構造函數後,再添加應該在子類中定義的屬性。
2.2 借用構造函數問題
若是僅僅是借用構造函數,那麼也沒法避免構造函數模式存在的問題——方法都在構造函數中定義,所以,函數的複用就無從談起。並且,在超類的原型中定義的方法,對子類而言也是不可見的,結果全部類型都只能使用構造函數模式。考慮到這個問題,借用構造函數的技術也是極少單獨使用的。
三、組合繼承
組合繼承,指的是將原型鏈繼承和借用構造函數的技術組合到一塊兒,從而發揮兩者之長的一種繼承模式。思路是:利用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。這樣,既經過在原型上定義方法實現了函數複用,又能保證每一個實例都有本身的屬性。
1 function SuperType(name) { 2 this.name = name; 3 this.colors = ["red","blue","green"]; 4 } 5 6 SuperType.prototype.sayName = function() { 7 console.log(this.name); 8 }; 9 10 function SubType(name,age){ 11 //繼承屬性 --從新建立SuperType構造函數屬性的副本 12 SuperType.call(this,name); 13 14 this.age = age; 15 } 16 17 //繼承方法 18 SubType.prototype = new SuperType(); 19 SubType.prototype.constructor = SubType; 20 SubType.prototype.sayAge = function() { 21 console.log(this.age); 22 }; 23 24 var instance1 =new SubType("Nicholas",29); 25 instance1.colors.push("black"); 26 console.log(instance1.colors); // "red,blue,green,black" 27 instance1.sayName(); // "Nicholas" 28 instance1.sayAge(); // 29 29 30 var instance2 = new SubType("Greg",22); 31 console.log(instance2.colors); // "red,blue,green" 32 instance2.sayName(); // "Greg" 33 instance2.sayAge(); // 22
組合繼承避免了原型鏈和借用構造函數的缺陷,融合它們的優勢,成爲JavaScript中最經常使用的繼承模式。
四、原型式繼承
這是另外一種繼承,沒有嚴格意義上的構造函數。思路是:藉助原型能夠基於已有的對象建立新對象,同時還沒必要要建立自定義類型。
1 function object(o) { 2 function F() {} 3 F.prototype = o; 4 return new F(); 5 }
在object()函數內部,先建立一個臨時的構造函數,而後將傳入的對象做爲構造函數的原型,最後返回這個臨時類型的一個新實例。從本質上講,object()對傳入其中的對象執行了一次淺複製。例子以下:
1 var person = { 2 name : "Nicholas", 3 friend : ["Shelby","Court","Van"] 4 }; 5 6 var anotherPerson = object(person); 7 anotherPerson.name = "Greg"; 8 anotherPerson.friends.push("Rob"); 9 10 var yetAnotherPerson = object(person); 11 yetAnotherPerson.name = "Linda"; 12 yetAnotherPerson.friends.push("Barbie"); 13 14 console.log(person.friends); // "Shelby,Court,Van,Greg,Barbie"
ECMAScript5經過Object.create()方法規範了原型式繼承。這個方法接受倆個參數:一個用做新對象原型的對象和(可選的)一個爲新對象定義額外屬性的對象。在傳入一個參數的狀況下,Object.create()與object()方法的行爲相同。支持Object.create()方法的瀏覽器有IE9+、Firefox4+、Safari5+、Opera12+和Chrome。
在沒有必要興師動衆地建立構造函數,而只想讓一個對象與另外一個對象保持相似的狀況下,原型式繼承是徹底能夠勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式同樣,一變全變!
五、寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路,與寄生式構造函數和工廠模式相似,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再像真地是它作了全部工做同樣返回對象。如下代碼示範了寄生式模式
1 function createAnother(original) { 2 var clone = object(original); //經過調用函數建立一個新對象 3 clone.sayHi = function() { //以某種方式來加強這個對象 4 console.log("hi"); 5 }; 6 return clone; // 返回這個對象 7 } 8 9 var person = { 10 name : "Nicholas", 11 friend : ["Shelby","Court","Van"] 12 }; 13 14 var anotherPerson = createAnother(person); //繼承person的屬性和方法,同時有本身的屬性和方法 15 anotherPerson.sayHi(); //hi
使用寄生式繼承來爲對象添加函數,會因爲不能作到函數的複用而下降效率;這一點和構造函數繼承模式相似。
六、寄生組合式繼承
前面說過,組合繼承是JavaScript最經常使用的繼承模式;不過,它也有本身的不足。組合繼承最大的問題就是不管什麼狀況下,都會調用倆次超類型構造函數SuperType():一次是在建立子類型原型的時候( SuperType.call(this,name); ),另外一個是在子類型構造函數內部( SubType.prototype = new SuperType(); )。
所謂寄生組合式繼承,即經過借用構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。寄生組合式繼承的基本思路:沒必要爲了指定子類型而調用超類型的構造函數,咱們所須要的無非就是超類型原型的一個副本而已。本質上,就是使用寄生式繼承超類型的原型,而後再將結果指定給子類型的原型。寄生組合式繼承的基本模式以下:
1 function inheritPrototype(subtype, supertype){ 2 var prototype = object(superType.prototype); // 建立對象 3 prototype.constructor = subType; // 加強對象 4 subType.prototype = prototype; // 指定對象 5 }
這個示例中的inheritPrototype()函數實現了寄生組合式繼承的最簡單形式。這個函數接收2個參數:子類型構造函數和超類型構造函數。在函數內部,第一步是建立超類型原型的一個副本。第二步是爲了建立的副本添加constructor屬性,從而彌補因重寫而失去的默認的constructor屬性,保證還能使用instanceof和isPrototypeOf()。最後一步,將新建的對象(即副本)賦值給子類型的原型。這樣,咱們就能夠用調用inheritPrototype()函數的語句,去替換前面例子中爲子類型原型賦值的語句( SubType.prototype = new SuperType(); )。
至此,JavaScript繼承的幾種經常使用方法到此結束,重點難點仍是要弄清構造函數、原型、實例之間的關係,什麼狀況下原型會被修改?怎樣繼承才能使原型不被修改?原型是怎樣被實例繼承的?構造函數屬性又是怎麼被實例繼承的(這個須要去了解 new都作了啥 這個知識點)?
最後,若發現錯誤之處,請留言告之,不勝感激!_^_
參考書籍:《高程》6.3