× 目錄 [1]原型繼承 [2]偽類繼承 [3]組合繼承 前面的話 學習如何創建對象是理解面向對象編程的第一步,第二步是理解繼承。本文是javascript面向對象系列第三篇——實現繼承的3種形式 【1】原型鏈 javascript使用原型鏈作為實現繼承的主要方法,實現的本質是重寫原型對象,代之以一 ...
×
目錄
[1]原型繼承 [2]偽類繼承 [3]組合繼承前面的話
學習如何創建對象是理解面向對象編程的第一步,第二步是理解繼承。本文是javascript面向對象系列第三篇——實現繼承的3種形式
【1】原型鏈
javascript使用原型鏈作為實現繼承的主要方法,實現的本質是重寫原型對象,代之以一個新類型的實例
function Super(){ this.value = true; } Super.prototype.getValue = function(){ return this.value; }; function Sub(){} //Sub繼承了Super Sub.prototype = new Super(); Sub.prototype.constructor = Sub; var instance = new Sub(); console.log(instance.getValue());//true
原型鏈最主要的問題在於包含引用類型值的原型屬性會被所有實例共用,而這也正是為什麼要在構造函數中,而不是在原型對象中定義屬性的原因。在通過原型來實現繼承時,原型實際上會變成另一個類型的實例。於是,原先的實例屬性也就順理成章地變成了現在的原型屬性了
function Super(){ this.colors = ['red','blue','green']; } function Sub(){}; //Sub繼承了Super Sub.prototype = new Super(); var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);//'red,blue,green,black' var instance2 = new Sub(); console.log(instance2.colors);//'red,blue,green,black'
【1.1】原型式繼承
原型式繼承藉助原型可以基於已有的對象創建新對象,同時還不必因此創建自定義類型。從本質上講,object()對傳入其中的對象執行了一次淺複製
function object(o){ function F(){}; F.prototype = o; return new F(); } var superObj = { colors: ['red','blue','green'] }; var subObj1 = object(superObj); subObj1.colors.push("black"); var subObj2 = object(superObj); subObj2.colors.push("white"); console.log(superObj.colors);//["red", "blue", "green", "black", "white"] console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]
實際上,Object.create()方法規範化了原型式繼承
var superObj = { colors: ['red','blue','green'] }; var subObj1 = Object.create(superObj); subObj1.colors.push("black"); var subObj2 = object(superObj); subObj2.colors.push("white"); console.log(superObj.colors);//["red", "blue", "green", "black", "white"] console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]
[註意]原型式繼承雖然只是看上去將原型鏈繼承的一些程式性步驟包裹在函數里而已,與原型鏈繼承有著引用類型值的問題。但是,它們的一個重要區別是父類型的實例對象不再作為子類型的原型對象
1、使用原型鏈繼承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; //將父類型的實例對象作為子類型的原型對象 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; //創建子類型的實例對象 var instance = new Sub; console.log(instance.value);//1
2、使用原型式繼承
function Super(){ this.value = 1; } Super.prototype.value = 0; function Sub(){}; Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; //創建子類型的實例對象 var instance = new Sub; console.log(instance.value);//0
上面的Object.create函數一行代碼Sub.prototype = Object.create(Super.prototype)可以分解為
function F(){}; F.prototype = Super.prototype; Sub.prototype = new F();
由上面代碼看出,子類的原型對象是臨時類F的實例對象,而臨時類F的原型對象又指向父類的原型對象;所以,實際上,子類可以繼承父類的原型上的屬性,但不可以繼承父類的實例上的屬性
【1.2】寄生式繼承
寄生式繼承創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後返回對象
function parasite(original){ var clone = Object.create(original);//通過調用函數創建一個新對象 clone.sayHi = function(){ //以某種方式來增強這個對象 console.log("hi"); }; return clone;//返回這個對象 } var superObj = { colors: ['red','blue','green'] }; var subObj1 = parasite(superObj); subObj1.colors.push('black'); var subObj2 = parasite(superObj); subObj2.colors.push('white'); console.log(superObj.colors);//["red", "blue", "green", "black", "white"] console.log(subObj1.colors);//["red", "blue", "green", "black", "white"]
[註意]寄生式繼承實際上只是原型式繼承的再包裝,與原型式繼承有著同樣的問題,且由於不能做到函數復用而降低了效率
【2】借用構造函數
借用構造函數(constructor stealing)的技術(有時候也叫做偽類繼承或經典繼承)。基本思想相當簡單,即在子類型構造函數的內部調用超類型構造函數,通過使用apply()和call()方法在新創建的對象上執行構造函數
function Super(){ this.colors = ['red','blue','green']; } function Sub(){ //繼承了Super Super.call(this); } var instance1 = new Sub(); instance1.colors.push('black'); console.log(instance1.colors);// ['red','blue','green','black'] var instance2 = new Sub(); console.log(instance2.colors);// ['red','blue','green']
相對於原型鏈而言,借用構造函數有一個很大的優勢,即可以在子類型構造函數中向超類型構造函數傳遞參數
function Super(name){ this.name = name; } function Sub(){ //繼承了Super,同時還傳遞了參數 Super.call(this,"bai"); //實例屬性 this.age = 29; } var instance = new Sub(); console.log(instance.name);//"bai" console.log(instance.age);//29
但是,如果僅僅是借用構造函數,那麼也將無法避免構造函數模式存在的問題——方法都在構造函數中定義,因此函數復用就無從談起了
【3】組合繼承
組合繼承(combination inheritance)有時也叫偽經典繼承,指的是將原型鏈和借用構造函數的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背後的思路是使用原型鏈實現對原型屬性和方法的繼承,而通過借用構造函數來實現對實例屬性的繼承。這樣,既通過在原型上定義方法實現了函數復用,又能夠保證每個實例都有它自己的屬性
function Super(name){ this.name = name; this.colors = ['red','blue','green']; } Super.prototype.sayName = function(){ console.log(this.name); }; function Sub(name,age){ //繼承屬性 Super.call(this,name); this.age = age; } //繼承方法 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ console.log(this.age); } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" instance1.sayAge();//29 var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu" instance2.sayAge();//27
組合繼承有它自己的問題。那就是無論什麼情況下,都會調用兩次父類型構造函數:一次是在創建子類型原型的時候,另一次是在子類型構造函數內部。子類型最終會包含父類型對象的全部實例屬性,但不得不在調用子類型構造函數時重寫這些屬性
function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ // 第二次調用Super(),Sub.prototype又得到了name和colors兩個屬性,並對上次得到的屬性值進行了覆蓋 Super.call(this,name); this.age = age; } //第一次調用Super(),Sub.prototype得到了name和colors兩個屬性 Sub.prototype = new Super(); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ return this.age; };
【3.1】寄生組合式繼承
寄生組合式繼承與組合繼承相似,都是通過借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。只不過把原型繼承的形式變成了寄生式繼承。使用寄生組合式繼承可以不必為了指定子類型的原型而調用父類型的構造函數,從而寄生式繼承只繼承了父類型的原型屬性,而父類型的實例屬性是通過借用構造函數的方式來得到的
function parasite(original){ var clone = Object.create(original);//通過調用函數創建一個新對象 return clone;//返回這個對象 } function Super(name){ this.name = name; this.colors = ["red","blue","green"]; } Super.prototype.sayName = function(){ return this.name; }; function Sub(name,age){ Super.call(this,name); this.age = age; } Sub.prototype = parasite(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.sayAge = function(){ return this.age; } var instance1 = new Sub("bai",29); instance1.colors.push("black"); console.log(instance1.colors);//['red','blue','green','black'] instance1.sayName();//"bai" instance1.sayAge();//29 var instance2 = new Sub("hu",27); console.log(instance2.colors);//['red','blue','green'] instance2.sayName();//"hu" instance2.sayAge();//27
最後
繼承這塊可能是ECMAScript中最難理解的部分。如果說作用域和this機制的難在於繞,則這部分的難則在於混雜。每種模式都有自己的優點,而多個模式結合在一起就可能造成一些屬性的重置,這是最需要註意的地方
更多的模式都是為了更好的解決問題。學習原理時學的深一點,解決問題時才能更順利點
以上