寫在前面的話:這篇博客不適合對面向對象一無所知的人,如果你連_proto_、prototype...都不是很了解的話,建議還是先去了解一下JavaScript面向對象的基礎知識,畢竟胖子不是一口吃成的。博文有點長,如果能仔細看懂每一句話(畢竟都是《高程3》的原話),收獲不容小覷。有關面向對象的基礎知識,請參見:JS的從理解對象到創建對象.
我們都知道面向對象語言的三大特征:繼承、封裝、多態,但JavaScript不是真正的面向對象,它只是基於面向對象,所以會有自己獨特的地方。這里就說說JavaScript的繼承是如何實現的。
學習過Java和c++的都知道,它們的繼承通過類實現,但JavaScript沒有類這個概念,那它通過什么機制實現繼承呢? 答案是: 原型鏈! 其基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
這篇博客主要是關於《高程3》—— 6.3 繼承 的總結,建議先閱讀阮一峰大神的js繼承三部曲,然后再回頭看體會更深:
下面這個是關於Function和Object創建實例之間的關系,不妨先了解一下它們之間復雜的關系:
圖 1
實現繼承之前,先看一個基於原型鏈繼承的鏈圖,對繼承有個具體化的概念: (這個是核心繼承部分)

圖 3
-------------------------------有了上面的思路,下面來看js中6種經典的實現繼承方法-----------------------------------------
導讀提示:方法1-3為一個體系,方法4-6為另一個體系.
1、原型鏈繼承
簡單回顧一下構造函數、原型和實例的關系:每個構造函數都有一個原型對象,原型對象都包含一個指向構造函數的指針(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中的實例屬性也變成可以被改寫的,不理想
第二個問題就是:在創建子類型的實例時,不能向超類的構造函數中傳遞傳遞參數。實際上,應該說是沒有辦法在不影響所有對象實例的情況下,給超類的構造函數傳遞參數。有鑒於此,再加上前面剛剛討論過的由於原型中包含引用類型值所帶來的問題,實踐中很少會單獨使用原型鏈繼承。
2、借用構造函數
為了解決原型鏈繼承帶來的問題,一種新的繼承應運而生——借用構造函數,其基本思想很簡單:在子類型構造函數的內部調用超類型。別忘了,函數只不過是在特定環境中執行的對象,因此通過使用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 借用構造函數問題
如果僅僅是借用構造函數,那么也無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此,函數的復用就無從談起。而且,在超類的原型中定義的方法,對子類而言也是不可見的,結果所有類型都只能使用構造函數模式。考慮到這個問題,借用構造函數的技術也是極少單獨使用的。
3、組合繼承
組合繼承,指的是將原型鏈繼承和借用構造函數的技術組合到一起,從而發揮二者之長的一種繼承模式。思路是:利用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能保證每個實例都有自己的屬性。
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中最常用的繼承模式。
4、原型式繼承
這是另一種繼承,沒有嚴格意義上的構造函數。思路是:借助原型可以基於已有的對象創建新對象,同時還不必要創建自定義類型。
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。
在沒有必要興師動眾地創建構造函數,而只想讓一個對象與另一個對象保持類似的情況下,原型式繼承是完全可以勝任的。不過別忘了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型模式一樣,一變全變!
5、寄生式繼承
寄生式繼承是與原型式繼承緊密相關的一種思路,與寄生式構造函數和工廠模式類似,即創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最后再像真地是它做了所有工作一樣返回對象。以下代碼示范了寄生式模式

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
使用寄生式繼承來為對象添加函數,會由於不能做到函數的復用而降低效率;這一點和構造函數繼承模式類似。
6、寄生組合式繼承
前面說過,組合繼承是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