第六章 物件導向的程式設計(二) JavaScript進階程式設計

來源:互聯網
上載者:User
6.2 繼承

許多OO語言都支援兩種繼承方式:介面繼承和實現繼承。介面繼承只繼承方法簽名,而實現繼承則繼承實際的方法。

由於函數沒有簽名,在ECMAScript中無法實現介面繼承。ECMAScript只支援實現繼承,而且其實現繼承主要是依靠原型鏈來實現的。

6.2.1 原型鏈

    原型鏈的基本思想是利用原型讓一個參考型別繼承另一個參考型別的屬性和方法。

建構函式、原型和執行個體的關係:每個建構函式都有一個原型對象,原型對象都包含一個指向建構函式的指標,而執行個體都包含一個指向原型對象的內部指標。

讓原型對象等於另一個類型的執行個體,此時的原型對象將包含一個指向另一個原型的指標,相應地,另一個原型中也包含一個指向另一個建構函式的指標。假如另一個原型又是另一個類型的執行個體,那麼上述關係依然成立,如此層層遞進,就構成了執行個體與原型的鏈條。這就是所謂原型鏈的基本概念。

<script>function SuperType(){this.property = true;}SuperType.prototype.getSuperValue = function(){return this.property;};function SubType(){this.subproperty = false;}//繼承了SuperTypeSubType.prototype = new SuperType();SubType.prototype.getSubValue = function(){return this.subproperty;};var instance = new SubType();console.log(instance.getSuperValue()); //trueconsole.log(instance instanceof Object); //trueconsole.log(instance instanceof SuperType); //trueconsole.log(instance instanceof SubType); //trueconsole.log(Object.prototype.isPrototypeOf(instance)); //trueconsole.log(SuperType.prototype.isPrototypeOf(instance)); //trueconsole.log(SubType.prototype.isPrototypeOf(instance)); //true</script>

1.別忘記預設的原型

所有參考型別預設都繼承了Object,而這個繼承也是通過原型鏈實現的。所有的函數的預設原型都是Object的執行個體,因此預設原型都會包含一個內部指標,指向Object.prototype。這也正是所有自訂類型都會繼承toString()、valueOf()等預設方法的根本原因。

2.確定原型和執行個體的關係

可以通過兩種方式來確定原型和執行個體之間的關係。第一種方式是使用instanceof操作符,只要用這個操作符來測試執行個體與原型鏈中出現過的建構函式,結果就會返回true。

第二種方式是使用isPrototypeof()方法。同樣,只要是原型鏈中出現過的原型,都可以說是該原型鏈所派生的執行個體的原型

3.謹慎地定義方法

子類型有時候需要重寫超類型中的某個方法,或者是需要添加超類型中不存在的某個方法。但不管怎樣,給原型添加方法的代碼一定要放在替換原型的語句之後。

<script>function SuperType(){this.property = true;}SuperType.prototype.getSuperValue = function(){return this.property;};function SubType(){this.subproperty = false;}//繼承了SuperTypeSubType.prototype = new SuperType();//添加新方法SubType.prototype.getSubValue = function(){return this.subproperty;};//重寫超類型中的方法SubType.prototype.getSuperValue = function(){//記住SuperType中的原型方法只是被屏蔽了!!!return false;}var instance = new SubType();console.log(instance.getSuperValue()); //false</script>

在通過原型鏈實現繼承時,不能使用對象字面量建立原型方法,這樣做就會重寫原型鏈。

<script>function SuperType(){this.property = true;}SuperType.prototype.getSuperValue = function(){return this.property;};function SubType(){this.subproperty = false;}//繼承了SuperTypeSubType.prototype = new SuperType();//使用字面量添加新方法,會導致上一行代碼無效SubType.prototype = {getSubValue : function(){return this.subproperty;},someOtherMethod : function(){return false;}}var instance = new SubType();console.log(instance.getSuperValue());//error</script>

4.原型鏈的問題

最主要的問題來自包含參考型別值的原型。

包含參考型別值的原型屬性會被所有執行個體共用,所以要在建構函式中,而不是在原型對象中定義屬性。

在通過原型來實現繼承時,原型實際上會變成另一個類型的執行個體。於是,原先的執行個體屬性也就變成了現在的原型屬性。

<script>function SuperType(){this.colors = ["red","blue","green"]}function SubType(){}//繼承了SuperTypeSubType.prototype = new SuperType();var instance1 = new SubType();instance1.colors.push("black"); //"red,blue,green,black"console.log(instance1.colors);var instance2 = new SubType();console.log(instance2.colors); //"red,blue,green,black"</script>

記住只要參考型別會出現這種問題,如果不是參考型別的話,子類嘗試修改prototype中的屬性,其實是在子類本身定義一個同名的屬性

<script>function SuperType(){this.colors = 5;}function SubType(){}//繼承了SuperTypeSubType.prototype = new SuperType();var instance1 = new SubType();instance1.colors = 6; //6console.log(instance1.colors);var instance2 = new SubType();console.log(instance2.colors); //5//主要的原因是在子類執行個體中是不能修改prototype中的屬性的//但是如果是參考型別,卻可以修改這個參考型別所指向的對象的屬性//記住上面的instance1.colors=6不是修改prototype中的屬性,而是自己自定了一個新的colorsdelete instance1.colors;console.log(instance1.colors);</script>

原型鏈的第二個問題是:在建立子類型的執行個體時,不能向超類型的建構函式中傳遞參數。實際上,應該說是沒有辦法在不影響所有對象執行個體的情況下,給超類型的建構函式傳遞參數。

6.2.2 借用建構函式            (偽造對象或經典繼承)

基本思想:在子類型建構函式的內部調用超類型建構函式。

函數只不過是在特定環境中執行代碼的對象,因此通過使用apply()和call()方法也可以在(將來)新建立的對象上執行建構函式。

<script>function SuperType(){this.colors = ["red","blue","green"]}function SubType(){//繼承了SuperTypeSuperType.call(this);}var instance1 = new SubType();instance1.colors.push("black");alert(instance1.colors); //"red,blue,green.black"var instance2 = new SubType();alert(instance2.colors); //"red,blue,green"</script>

1.傳遞參數

可以在子類型建構函式中向超類型建構函式傳遞參數。

<script>function SuperType(name){this.name = name;}function SubType(){//繼承了SuperType,,同時還傳遞了參數SuperType.call(this,"Nicholas");//執行個體屬性this.age = 29;}var instance = new SubType();console.log(instance.name); //"Nicholas"console.log(instance.age); //29</script>

為了確保SuprType建構函式不會重寫子類型的屬性,可以在調用超類型建構函式後,再添加應該在子類型中定義的屬性

2.借用建構函式的問題

如果僅僅是借用建構函式,那麼也將無法避免建構函式模式存在的問題————方法都在建構函式中定義,因此函數複用也就無從談起了。而且,在超類型的原型中定義的方法,對子類型而言也是不可見的,結果所有類型都只能使用建構函式模式。

<script>function SuperType(name){this.name = name;}SuperType.prototpe.getName = function(){console.log(this.name);}function SubType(){//繼承了SuperType,,同時還傳遞了參數SuperType.call(this,"Nicholas");//執行個體屬性this.age = 29;}var instance = new SubType();console.log(instance.name); //"Nicholas"console.log(instance.age); //29console.log(instance.getName());//erros</script>

6.2.3 組合繼承(偽經典繼承)

將原型鏈和借用建構函式的技術組合到一塊,從而發揮二者之長的一種繼承模式。其背後的思想是使用原型鏈實現對原型屬性和方法的繼承,而通過借用建構函式來實現對執行個體屬性的繼承。這樣,既通過在原型上定義方法實現了函數複用,又能夠保證每個執行個體都有它自己的屬性。

6.2.4 原型式繼承

借組原型可以基於已有的對象建立新對象,同時還不必因此建立自訂類型。

<script>//在object()函數內部,先建立了一個臨時性的建構函式,然後將傳入的對象作為這個建構函式的原型,//最後返回了這個臨時類型的一個新執行個體。//從本質上將,object()對傳入其中的對象執行了一次淺複製。function object(o){function F(){}F.prototype = o;return new F();}var person = {name :"Nicholas",friends:["Shellby","Court","Van"]};var anotherPerson = object(person);anotherPerson.name = "Greg";anotherPerson.friends.push("Rob");var yetAnotherPerson = object(person);yetAnotherPerson.name = "Linda";yetAnotherPerson.friends.push("Barbie");console.log(person.friends);console.log(anotherPerson.friends);console.log(yetAnotherPerson.friends);</script>

包含參考型別值的屬性始終都會共用相應的值,就像使用原型模式一樣。

在沒有必要興師動眾地建立建構函式,而只想讓一個對象與另一對象保持類似的情況下,原型式繼承是完全可以勝任的。

6.2.5 寄生式繼承

建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來增強對象,最後再想真地是它做了所有工作一樣返回對象。

<script>function object(o){function F(){}F.prototype = o;return new F();}function createAnother(original){var clone = object(original); //通過調用函數來建立一個新對象clone.sayHi = function(){     //以某種方式來增強這個對象console.log("Hi");};return clone;                  //返回這個對象}var person = {name : "Nicholas",friends : ["Shelby","Court","Van"]};var anotherPerson = createAnother(person);anotherPerson.sayHi();  //"Hi"</script>

在主要考慮對象而不是自訂類型和建構函式的情況下,寄生式繼承也是一種有用的模式。前面示範繼承模式時使用的object()函數不是必須的;任何能夠返回新對象的函數都適用於此模式。

使用寄生式繼承來為對象添加函數,會由於不能做到函數複用而降低效率;這一點與建構函式模式類似。

6.2.6 寄生組合式繼承

組合繼承最大的問題是無論什麼情況下,都會調用兩次超類型建構函式:一次是在建立子類型原型的時候,另一次是在子類型建構函式內部。子類型最終會包含超類型對象的全部執行個體屬性,但不得不在調用子類型建構函式時重寫這些屬性。

<script>function SuperType(name){this.name = name;this.colors = ["red","blue","green"];}SuperType.prototype.sayName = function(){console.log(this.name);};function SubType(name,age){SuperType.call(this,name); //第二次調用SuperTypethis.age = age;}SubType.prototype = new SuperType(); //第一次調用SuperType()SubType.prototype.sayAge = function(){console.log(this.age);};</script>

寄生組合式繼承,即通過借用建構函式來繼承屬性,通過原型鏈的混成形式來繼承方法。其背後的基本思想是:不必為了指定子類型的原型而調用超類型的建構函式,我們所需要的無非就是超類型的一個副本而已。本質上,就是使用寄生式繼承來繼承超類型的原型,然後再將結果指定給子類型的原型。

<script>function inheritPrototype(subType,superType){var prototype = object(superType.prototype); //建立對象prototype.constructor = subType;//增強對象subType.prototype = prototype; //指定對象}function SuperType(name){this.name = name;this.colors = ["red","blue","green"];}SuperType.prototype.sayName = function(){console.log(this.name);};function SubType(name,age){SuperType.call(this,name);this.age = age;}inheritPrototype(SubType,SuperType);SubType.prototype.sayAge = function(){console.log(this.age);};</script>

這個例子的高效率體現在它只調用了一次SuperType建構函式,並且因此避免了在SubType.prototype上面建立不必要的、多餘的屬性。與此同時,原型鏈還能保持不變;因此,還能夠正常使用instanceof和isPrototypeOf()。
6.3小結

對象可以在代碼執行過程中建立和增強,因此具有動態性而非嚴格定義的實體。

建立對象的模式:

  • 原廠模式,使用簡單的函數建立對象,為對象添加屬性和方法,然後返回對象。
  • 建構函式模式,可以建立自訂參考型別,可以像建立內建對象執行個體一樣使用new操作符。不過,建構函式模式也有缺點,即它的每個成員都無法得到複用,包括函數。由於函數可以不局限於任何對象(即與對象具有鬆散耦合的特點),因此沒有理由不在多個對象間共用函數。
  • 原型模式,使用建構函式的prototype屬性來指定那些應該共用的屬性和方法。組合使用建構函式模式和原型模式時,使用建構函式定義執行個體屬性,而使用原型定義共用的屬性和方法。

JavaScript主要通過原型鏈實現繼承。原型鏈的構建是通過將一個類型的執行個體賦值給另一個建構函式的原型實現的。這樣,子類型就能夠訪問超類型的所有屬性和方法,這一點與基於類的繼承很相似。原型鏈的問題是對象執行個體共用所有繼承的屬性和方法,因此不適合單獨使用。解決這個問題的技術是借用建構函式,即在子類型建構函式的內部調用超類型建構函式。這樣就可以做到每個執行個體都具有自己的屬性,同時還能保證只使用建構函式模式來定義類型。使用最多的繼承模式是組合繼承,這種模式使用原型鏈繼承共用的屬性和方法,而通過借用建構函式繼承執行個體屬性。

  • 原型式繼承,可以在不必預先定義建構函式的情況下實現繼承,其本質是執行對給定對象的淺複製。而複製得到的副本還可以得到進一步改造。
  • 寄生式繼承,與原型式繼承非常相似,也是基於某個對象或某些資訊建立一個對象,然後增強對象,最後返回對象。為瞭解決組合繼承模式由於多次調用超類型建構函式而導致的低效率問題可以將這個模式與組合繼承一起使用。
  • 寄生組合式繼承,集寄生式繼承和組合繼承的優點與一身,是實現基於類型繼承的最有效方式。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.