TDD測試驅動的javascript開發(3) ------ javascript的繼承
說起麵向對象,人們就會想到繼承,常見的繼承分為2種:接口繼承和實現繼承。接口繼承隻繼承方法簽名,實現繼承則繼承實際的方法。
由於函數沒有簽名,在ECMAScript中無法實現接口繼承,隻支持實現繼承。
1. 原型鏈
1.1 原型鏈將作為實現繼承的主要方法,基本思想是利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
構造函數---原型---實例 之間的關係:
每一個構造函數都有一個原型對象,原型對象包含一個指向構造函數的指針,而實例都包含一個指向原型對象的內部指針。
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); //通過原型鏈實現繼承 SubType.prototype.getSubValue = function() { return this.subproperty; }; var subInstance = new SubType(); var superInstance = new SuperType(); TestCase("test extends",{ "test superInstance property should be true" : function() { assertEquals(true,superInstance.property); }, "test superInstance getSuperValue() should be return true" : function() { assertEquals(true,superInstance.getSuperValue()); }, "test subInstance property should be false" : function() { assertEquals(false,subInstance.subproperty); }, "test subInstance could visit super method " : function() { assertEquals(true,subInstance.getSuperValue()); //SubType繼承SuperType,並調用父類的方法 } });
注:要區分開父類和子類的屬性名稱,否則子類的屬性將會覆蓋父類的同名屬性值:看如下代碼:
function SubType() { this.property = false; } SubType.prototype.getSubValue = function() { return this.property; }; function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; SubType.prototype = new SuperType(); //通過原型鏈實現繼承 var subInstance = new SubType(); var superInstance = new SuperType(); TestCase("test extends",{ "test superInstance property should be true" : function() { assertEquals(true,superInstance.property); //父類的property值為true }, "test superInstance getSuperValue() should be return true" : function() { assertEquals(true,superInstance.getSuperValue()); //superInstance調用方法 }, "test subInstance property should be false" : function() { assertEquals(false,subInstance.property); //子類的property屬值為false }, "test subInstance could visit super method " : function() { assertEquals(false,subInstance.getSuperValue()); //SubType繼承SuperType,並調用父類的方法,可以屬性被覆蓋了,返回false } });續:當然,如果我們不要求對屬性值進行初始化的時候,就不必考慮這個問題,我們會采用上一章講的構造函數模式+原型模式來創建類和實現繼承關係。
當以讀取模式訪問一個實例屬性時,首先會在實例中搜索該屬性,如果沒有找到該屬性,則繼續在實例的原型中尋找。在通過原型鏈實現繼承的情況下,會繼續沿著原型鏈繼續向上
subExtends.getSuperValue()首先在實例中查找,然後在SubType.prototype,最後在SuperType.prototype中找到。
補充: 所有函數的默認原型都是Object的實例,因此默認原型都會包含一個內部指針,指向Object.prototype,這也正是所有自定義類型都會繼承toString()、valueOf()等默認方法的原因。
1.2 確定原型和實例的關係
方法一:使用instanceof 操作符 ---- 隻要該實例是原型鏈中出現過的構造函數,結果就會返回true
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); //通過原型鏈實現繼承 SubType.prototype.getSubValue = function() { return this.subproperty; }; var subInstance = new SubType(); TestCase("test extends",{ "test subInstance should instanceof Object" : function() { assertInstanceOf(Object,subInstance); }, "test subInstance should instanceof SuperType " : function() { assertInstanceOf(SuperType,subInstance); }, "test subInstance should instanceof SubType " : function() { assertInstanceOf(SubType,subInstance); } });
方法二:使用isPrototypeOf()方法 ---- 隻要原型鏈中出現過該原型,都可以說是該原型鏈所派生的實例的原型,結果會返回true
function SuperType() { this.property = true; } SuperType.prototype.getSuperValue = function() { return this.property; }; function SubType() { this.subproperty = false; } SubType.prototype = new SuperType(); //通過原型鏈實現繼承 SubType.prototype.getSubValue = function() { return this.subproperty; }; var subInstance = new SubType(); TestCase("test extends",{ "test subInstance isPrototypeOf Object" : function() { assertEquals(true,Object.prototype.isPrototypeOf(subInstance)); }, "test subInstance isPrototypeOf SuperType " : function() { assertEquals(true,SuperType.prototype.isPrototypeOf(subInstance)); }, "test subInstance isPrototypeOf SubType " : function() { assertEquals(true,SubType.prototype.isPrototypeOf(subInstance)); } });
注:在實踐中,我們很少會單獨的使用原型鏈,因為它存在兩個問題:
一、引用類型值的原型:包含引用類型值的原型屬性會被所有實例共享,這也正是為什麼要在構造函數中定義屬性,而不在原型中定義屬性的原因
二、創建子類型的實例時,不能向超類型的構造函數中傳遞參數。
1.3 借用構造函數(偽造對象 ---- 經典繼承)
原理: 在子類型構造函數的內部調用超類型的構造函數
function SuperType(name) { this.name = name; this.friends = ['tong','feng']; } function SubType(name,age) { SuperType.call(this,name); } var subInstance = new SubType("tongtong",26); subInstance.friends.push('ty'); var subInstance2 = new SubType("fengfeng",27); TestCase("test constructor extends",{ "test subInstance friends property" : function() { assertEquals("ty",subInstance.friends[2]); //將ty push到數組 }, "test subInstance2 friends length" : function() { assertEquals(2,subInstance2.friends.length); //subInstance2 friends屬性沒有改變 } });
1.4 組合繼承(經典偽繼承) ----- 推薦模式
將原型鏈和借用構造函數的技術組合到一塊,使用原型鏈實現對原型屬性和方法的繼承,通過借用構造函數來實現對實例屬性的繼承
function SuperType(name) { this.name = name; this.friends = ['tong','feng']; } SuperType.prototype.sayName = function() { return this.name; }; function SubType(name,age) { //繼承屬性 SuperType.call(this,name); //調用構造函數 this.age = age; } //繼承方法 SubType.prototype = new SuperType(); //調用構造函數 SubType.prototype.sayAge = function() { return this.age; }; var subInstance = new SubType("tongtong",26); subInstance.friends.push('ty'); var subInstance2 = new SubType("fengfeng",27); TestCase("test extends",{ "test subInstance name property" : function() { assertEquals("tongtong",subInstance.sayName()); }, "test subInstance2 name property" : function() { assertEquals("fengfeng",subInstance2.sayName()); }, "test subInstance friends property" : function() { assertEquals("ty",subInstance.friends[2]); //將ty push到數組 }, "test subInstance2 friends length" : function() { assertEquals(2,subInstance2.friends.length); //subInstance2 friends屬性沒有改變 } });
這種繼承的缺點:將會2次調用構造函數,性能一般,解決辦法:參見1.7寄生組合式繼承
1.5 原型式繼承
這種方法沒有嚴格意義上的構造函數,思想是借助原型可以基於已有的對象創建新的對象,同時還不必因此創建自定義類型。
它要求你必須有一個對象可以作為另一個對象的基礎。如果有這麼一個對象,可以把它傳遞給object()函數,然後該函數就會返回一個新對象。
function object(o) { function F() { } F.prototype = o; return new F(); } var person = { name : "tongtong", friends : [ "feng", "tong" ] }; var another = object(person); another.name = "newtong"; another.friends.push('lan'); var other = object(person); TestCase("test prototype extends",{ "test another is extends person name property" : function() { assertEquals("newtong",another.name); }, "test person firends property length is 3" : function() { assertEquals(3,person.friends.length); }, "test other firends property length is 3" : function() { assertEquals(3,other.friends.length); } });
person 作為另一個對象的基礎,我們把它傳入到object()中,然後該函數就會返回一個新對象,這個對象將person做為原型。
ECMAScript通過Object.create()方法規範了原型式繼承,在傳入一個參數的時候,Object.create()與object()方法的行為相同。
var person = { name : "tongtong", friends : [ "feng", "tong" ] }; var another = Object.create(person); another.name = "lisa"; another.friends.push("lan"); TestCase("test prototype extends",{ "test another is extends person name property" : function() { assertEquals("lisa",another.name); }, "test person firends property length is 3" : function() { assertEquals(3,person.friends.length); } });
如果傳入2個參數,第二個參數會覆蓋同名參數
var person = { name : "tongtong", friends : [ "feng", "tong" ] }; var another = Object.create(person, { name : { value : "claire" } }); another.friends.push('lalala'); TestCase("test prototype extends",{ "test another is extends person name property" : function() { assertEquals("claire",another.name); }, "test person firends property length is 3" : function() { assertEquals(3,person.friends.length); } });
在隻想讓一個對象與另一個對象保持類似的情況下,又沒必要創建構造函數的時候,原型式繼承OK.(它和原型模式一樣哦,引用類型的值都會被共享)。
1.6寄生式繼承
思路:與寄生構造和工廠模式類似,創建一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再像它真的做了所以工作一樣返回。
//這是一個函數哦 function createAnother(original) { // var clone = object(original); //創建一個新對象 var clone = Object.create(original); clone.sayThis = function() { //增強對象 return original; }; return clone; //返回對象 }; var person = { name : "tong", friends : ['tong','feng'] }; var another = createAnother(person); TestCase("test prototype extends",{ "test another is extends person name property" : function() { assertEquals("tong",another.sayThis().name); } }); ;缺點:使用寄生式繼承來為對象添加函數,會因為函數不能複用而降低效率(和構造函數模式相似)
1.7寄生組合式繼承 ------ 最佳方案
所謂寄生組合式繼承,即通過借用構造函數來繼承屬性,通過原型鏈的混合形式來繼承方法,
思路:不必為了指定子類型的原型而調用超類型的構造函數,我們所需的無非就是超類型原型的一個副本而已。
本質上:使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型
特點:寄生式繼承是引用類型最理想的繼承方式
function SuperType(name) { this.name = name; this.colors = ['red','green']; } SuperType.prototype.sayName = function() { return this.name; }; /** * 1.創建父類型的一個副本 * 2.彌補創建副本時所丟失的constructor屬性 * 3.將新的副本賦值給子類型的原型 * @param subType * @param superType */ function inheritPrototype(subType,superType) { var object = Object.create(superType.prototype);//創建對象 object.constructor = subType; //增強對象 subType.prototype = object; //指定對象 } function SubType(name,age) { SuperType.call(this,name); //調用構造函數 this.age = age; } inheritPrototype(SubType,SuperType); SubType.prototype.sayAge = function() { return this.age; }; var subInstance1 = new SubType('feng',28); subInstance1.colors.push('yellow'); var subInstance2 = new SubType('tong',25); TestCase("test parasitic extends",{ "test subInstance1 name property should be feng" : function() { assertEquals("feng",subInstance1.name); }, "test subInstance2 name property should be tong" : function() { assertEquals('tong',subInstance2.name); }, "test subInstance1 colors property length should be 3" : function() { assertEquals(3,subInstance1.colors.length); }, "test subInstance2 colors property length should be 2" : function() { assertEquals(2,subInstance2.colors.length); }, "test subInstance1 sayAge method should be return 28" : function() { assertEquals(28,subInstance1.sayAge()); }, "test subInstance2 sayAge method should be return 25" : function() { assertEquals(25,subInstance2.sayAge()); }, "test subInstance1 should be instanceof SuperType" : function() { assertInstanceOf(SuperType,subInstance1); } });
1.8總結
1.8.1 ECMAScript 支持麵向對象編程,但不使用類或者接口,對象可以在代碼執行過程中創建和增強。
1.8.2 創建對象的幾種方式:
構造函數模式:可以創建自定義的引用類型,使用new操作符
缺點:在每個實例上都要重新創建,無法複用,包括函數
優點:與對象的鬆耦合
適用場合:當屬性或者方法不做共享屬性或者方法的時候(比如引用類型的屬性),不建議單獨使用。
原型模式:使用構造函數的prototype屬性來指定那些共享的屬性和方法
優點:所有成員都可以共享屬性和方法
缺點:沒有私有屬性值
適用場合:所以的屬性和方法都可以被共享的時候,不建議單獨使用
組合使用構造函數和原型模式:使用構造函數定義實例屬性,使用原型定義共享屬性和方法。
優點:解決了原型模式共享引用類型的屬性的問題,也解決了構造函數不能共享屬性的問題。
缺點:實現繼承的時候,將會2次調用父類型的構造函數。性能問題。
使用場合:使用最廣泛的定義引用類型的一種默認模式。
動態原型模式:保持了構造函數和原型的優點,把所有的信息封裝在構造函數內,在有必要的情況下進行初始化原型。
穩妥構造函數模式:適合在安全環境中使用。
1.8.3 javascript的繼承:
javascript主要通過原型鏈事實現繼承。
原型鏈的繼承:原型鏈的構建是通過將一個類型的實例賦值給另一個構造函數的原型實現的。
缺點:對象實例共享所有繼承的屬性和方法,因此不宜單獨使用。
借用構造函數繼承:在子類構造函數的內部調用超類型的構造函數。call() apply()
缺點:沒有函數的複用,不建議單獨使用
組合繼承:使用原型鏈繼承共享的屬性和方法,而通過借用構造函數繼承實例屬性。
缺點:將會調用兩次構造函數,會有性能問題
原型式繼承:可以在不必預定義構造函數的情況下實現繼承,其本質是執行對給定對象的淺複製,而複製的副本還可以得到進一步的改造。
優點:在沒有必要創建構造函數,隻是讓一個對象與另一個對象保持類似的情況下,原型式繼承OK.
缺點:共享屬性和方法
寄生式繼承:與原型式繼承非常相似,基於對象獲某些信息創建一個對象,然後增強對象,最後返回對象
優點:解決組合繼承多次調用父類的構造函數而導致低效率問題。(可以與組合模式一起使用)
缺點:使用寄生式繼承來為對象添加函數,會由於不能做到函數複用而降低效率,與構造函數模式類似。
寄生組合式繼承:集寄生式繼承和組合繼承的優點於一身,是實現基於類型繼承的最有效方式。
最後更新:2017-04-03 15:21:43