javascript 學習筆記之面向對象編程(二):繼承&多態

~~接上篇~~上一篇實現了類的實現以及類成員變量和方法的定義,下面咱們來了解下面向對象中兩個最重要的特性:繼承和多態web

繼承編程

js中一樣能夠實現類的繼承這一面向對象特性,繼承父類中的全部成員(變量和屬性),同時可擴展本身的成員,下面介紹幾種js中實現繼承的方式:app

1. 對象模仿(冒充):經過動態改變this指針的指向,實現對父類成員的重複定義,以下: 模塊化

 1 //對象冒充
 2 function ClassA(paramColor) {
 3     this.color = paramColor;
 4     this.sayColor = function() {
 5         alert(this.color);
 6     };
 7 }
 8 
 9 function ClassB(paramColor, name) {
10     //冒充並實現ClassA中的成員
11     this.newMethod = ClassA;
12     this.newMethod(paramColor);
13     //刪除掉對ClassA類冒充所使用的函數對象。
14     delete this.newMethod;
15 
16     this.name = name;
17     this.sayName = function() {
18         alert(this.name);
19     };
20 }
21 
22 var obj = new ClassB("yellow", "apple");
23 
24 console.log("實例obj是不是ClassA的對象" + (obj instanceof ClassA));
25 console.log("實例obj是不是ClassB的對象" + (obj instanceof ClassB));

上例中咱們實現了兩個類,ClassA和ClassB,在ClassB的實現過程當中,定義了一個函數newMethod來引用ClassA的構造函數並執行,這樣就等於執行了A的構造函數,只不過此時的this指針指向的是類ClassB,故ClassA構造函數的這個模仿執行過程實際上是給ClassB定義了相同的成員,最後刪除這個起橋樑性質的冒充函數,執行結果以下:函數

根據執行結果咱們能夠看出,子類ClassB定義的對象並不一樣屬其父類的實例,這種方式實現的繼承並非實際意義上的繼承, 此外,這種方式只能模仿實現父類構造函數中定義的成員,對於父類中經過prototype定義的成員將不能繼承。優化

2. 利用apply和call方法實現繼承:同第一種方式類似,這種方式是經過apply和call方法動態改變this指針的引用實現對父類成員的重複定義,下面對ClassB改寫以下:this

 1 //call方法
 2 function ClassBEx(paramColor, name) {
 3     ClassA.call(this, paramColor);
 4 
 5     this.name = name;
 6     this.sayName = function() {
 7         alert(this.name);
 8     }
 9 }
10 //aply方法
11 function ClassBEEx(paramColor, name) {
12     //若是類A的構造函數與類B的構造函數參數順序徹底相同時可用
13     ClassA.apply(this, arguments); 
14     
15     this.name = name;
16     this.sayName = function() {
17         alert(this.name);
18     }
19 }

這種方式同上一種的優缺點同樣,並非實際意義上的繼承。spa

3. 共享prototype對象實現繼承:子類經過對父類prototype對象進行共享以對父類成員的定義,從而實現繼承,下面對ClassA和ClassB進行從新定義:prototype

 1 //類ClassA的定義
 2 function ClassA(paramColor) {
 3     this.color = paramColor;
 4 }
 5 ClassA.prototype.sayColor = function() {
 6     console.log("執行ClassA中的成員函數sayColor:" + this.color);
 7 }
 8 //類ClassB的定義
 9 function ClassB(paramColor, name) {
10     this.name = name;
11 }
12 //類ClassB共享使用類ClassA的prototype對象
13 ClassB.prototype = ClassA.prototype; 14 ClassB.prototype.sayName = function() {
15     console.log(this.name);
16 }
17 //ClassB重寫了類ClassA中的函數成員
18 ClassB.prototype.sayColor = function() {
19     console.log(this.color);
20 }
21 
22 var objA = new ClassA("yellow");
23 var obj = new ClassB("red","apple");
24 
25 console.log("實例obj的color屬性" + obj.color);
26 console.log("實例obj是不是ClassA的對象" + (obj instanceof ClassA));
27 console.log("實例obj是不是ClassB的對象" + (obj instanceof ClassB));
28 objA.sayColor();

上面陰影部分代碼實現子類ClassB對父類ClassA的prototype對象進行共享,執行結果以下:指針

結果有點點意外,能夠總結爲如下幾點:

1. 共享prototype對象能夠實現子類的實例同屬於父類的實例,這點可經過 instance of 返回爲true看出;

2. 這種方式的繼承只能繼承父類prototype中的定義的父類成員,對於父類構造函數中的成員則不能繼承,如上圖:子類實例obj的color屬性爲undefined。

3. 共享原型(prototype)法,其實是使父類和子類的都引用同一個prototype對象,js中除了基本數據類型(數值、字符串、布爾類等),全部的賦值都是引用傳遞,而不是值傳遞,上述的共享致使ClassA和ClassB的prototype對象始終保持一致,因此當子類ClassB重複定義了父類中的sayColor函數後,父類中的sayColor也一樣更新了,故調用父類sayColor後輸出的是「red」。

4. 共享原型方法會致使基類和派生類定義本身的成員時互相干擾。

總之,此方法仍是不能實現實際意義上的繼承。

4. 經過反射機制和prototype實現繼承:在共享原型的基礎上進行了改進,經過遍歷基類的原型對象來給派生類原型對象賦值,以達到繼承的目的,具體以下:

 1 //類ClassA的定義
 2 function ClassA(paramColor) {
 3     this.color = paramColor;
 4 }
 5 ClassA.prototype.sayColor = function() {
 6     console.log("執行ClassA中的成員函數sayColor:" + this.color);
 7 }
 8 //類ClassB的定義
 9 function ClassB(paramColor, name) {
10     this.name = name;
11 }
12 //遍歷基類的原型對象來給本身的原型賦值
13 for (var p in ClassA.prototype) { 14     ClassB.prototype[p] = ClassA.prototype[p]; 15 } 16 ClassB.prototype.sayName = function() {
17     console.log(this.name);
18 }
19 //ClassB重寫了類ClassA中的函數成員
20 ClassB.prototype.sayColor = function() {
21     console.log("執行ClassB中的成員函數sayColor:red");
22 }
23 
24 var objA = new ClassA("yellow");
25 var obj = new ClassB("red", "apple");
26 
27 console.log("實例obj的color屬性" + obj.color);
28 console.log("實例obj是不是ClassA的對象" + (obj instanceof ClassA));
29 console.log("實例obj是不是ClassB的對象" + (obj instanceof ClassB));
30 objA.sayColor();
31 obj.sayColor();

上面陰影部分的代碼爲遍歷基類(ClassA)的prototype對象而後賦值給派生類ClassB的prototype對象,實現對基類的成員進行繼承,執行結果以下:

  由上圖可見,基類和派生類的prototype是獨立的,派生類繼承了基類prototype定義的成員,並添加和重寫了基類的成員函數sayColor,它們的執行結果互不干擾,惟一的缺憾是當前這種方式仍然不能繼承基類構造函數中定義的成員,這一點能夠經過在派生類的構造函數中添加一行代碼實現,改寫派生類ClassB的定義以下:

1 //類ClassB的定義
2 function ClassB(paramColor, name) {
3     ClassA.call(this, paramColor); 4     this.name = name;
5 }

這樣將基類的構造函數經過this指針附加到派生類的執行上下文中執行,實現對基類構造函數中定義的成員的繼承。

爲了提升代碼的可讀性,咱們改進遍歷基類prototype的實現過程: 

1 Function.prototype.inherit = function(superClass) {
2     for (var p in superClass.prototype) {
3         this.prototype[p] = superClass.prototype[p];
4     }
5 }

經過給Function對象添加成員方法,咱們給全部的函數類型對象添加了一個靜態方法,實現類的繼承咱們能夠經過下面這句代碼:

1 ClassB.inherit(ClassA);

從繼承的角度,上面這種方式更加容易被接受,可是有一點,經過反射(遍歷)結合prototype實現繼承的派生類,若是須要額外定義本身的成員,則只能經過對ptototype對象定義新的屬性(ClassB.prototype.newAttr=?)來實現,而不能經過無類型方式(ClassB.prototype={}),不然會覆蓋掉從基類繼承下來的成員。

5. 繼承的優化:主要對最後一種繼承機制進行優化,定義一個Extend函數,實現對從基類繼承後的對象的一個擴展,從而使得派生類添加新成員時更加高效,代碼實現以下:

 1 /*
 2 *  將對象p中的屬性所有添加到o對象中,若是存在重複,則直接覆蓋    
 3 */
 4 function extend(o, p) {
 5     for (prop in p) {
 6         o[prop] = p[prop];
 7     }
 8     return o;
 9 }
10 /*
11  *    建立以o對象爲原型的新的對象。
12  *  新的對象包含o中全部的成員
13  */
14 function inherit(o) {
15     if (o == null) throw TypeError();
16     if (Object.create) {
17         return Object.create(o);
18     }
19     var t = typeof p;
20     if (t !== "Object" && t !== "function") throw TypeError();
21     function f() { }
22     f.prototype = o;
23     return new f();
24 }
25 /*
26  *    經過Function給每一個函數對象添加一個靜態方法
27  *  constructor:派生類構造函數
28  *  methods:派生類須要新定義的成員方法
29  *  statics:派生類須要定義的靜態變量或方法的集合
30  *  返回派生類構造函數
31  */
32 Function.prototype.extend = function(constructor, methods, statics) { 33     return definedSubClass(this, constructor, methods, statics); 34 } 35 /*
36  *    js類繼承的核心方法
37  *  superClass:基類的構造函數(extend的執行時this指針,執行函數對象自己)
38  *  constructor:派生類構造函數
39  *  methods:派生類須要新定義的成員方法
40  *  statics:派生類須要定義的靜態變量或方法的集合
41  *  返回派生類構造函數
42  */
43 function definedSubClass(superClass, constructor, methods, statics) {
44     constructor.prototype = inherit(superClass.prototype);
45     constructor.prototype.constructor = constructor;
46     if (methods) extend(constructor.prototype, methods);
47     if (statics) extend(cosntructor, statics);
48     return constructor;
49 }

這些都是實現類繼承模板的核心函數,主要是經過Function對象給全部的函數類型的對象添加了一個靜態函數,有了上面的函數,實現上面ClassB繼承ClassA,咱們能夠改成成:

 1 //類ClassA的定義
 2 function ClassA(paramColor) {
 3     this.color = paramColor;
 4 }
 5 ClassA.prototype.sayColor = function() {
 6     console.log("執行ClassA中的成員函數sayColor:" + this.color);
 7 }
 8 
 9 //ClassA做爲基類派生出ClassB
10 var ClassB = ClassA.extend(function(paramColor, name) { 11     //構造函數(成員屬性由構造函數定義)
12     ClassA.call(this, paramColor); 13     this.name = name; 14 }, { 15     //新定義或者從新定義的方法
16     sayName: function() { 17         console.log(this.name); 18  }, 19     sayColor: function() { 20         console.log("執行ClassB中的成員函數sayColor:red"); 21  } 22 }, 23 { 24     //無靜態成員
25 }); 26 
27 var objA = new ClassA("yellow");
28 var obj = new ClassB("red", "apple");
29 
30 console.log("實例obj的color屬性" + obj.color);
31 console.log("實例obj是不是ClassA的對象" + (obj instanceof ClassA));
32 console.log("實例obj是不是ClassB的對象" + (obj instanceof ClassB));
33 objA.sayColor();
34 obj.sayColor();

陰影部分,咱們經過擴展的extend函數實現了類的繼承,簡單明瞭,執行上面的例子,結果以下:

能夠看出,優化後的方法完美的實現了js類的繼承中遇到的幾個問題。

多態

  面向對象編程中的多態主要是經過抽象類和抽象函數實現的,js中也能夠從這兩個方面實現多態。傳統意義上的多態,是經過派生類繼承並實現基類中的抽象(虛)函數來實現的,含有抽象函數的類是抽象類,抽象類是不可以實例化的,同時,抽象函數沒有函數體,也不可以直接調用,只能有派生類繼承並實現。在高級程序語言中,上述這些檢測均在程序編譯時進行,不符合要求的程序編譯將不經過,可是在js中,有了些許變化:

1. js是解釋性語言,不須要進行預編譯,因此js中抽象類和抽象函數的使用並無那麼嚴格的要求。

2. js中能夠對未定義的方法進行調用,固然這一過程會報錯,而檢測時在執行調用時進行的。

因此,js中的抽象類能夠定義實例,但就其意義而言,咱們能夠定義一個空的沒有成員的類來代替,一樣,js中的抽象函數,咱們能夠沒必要在基類中聲明,直接進行調用,在派生類中實現便可,固然,也能夠經過在基類中定義一個空的抽象方法實現,代碼以下:

 1 function ClassA() {
 2     //抽象類,類的實現過程爲空
 3 }
 4 ClassA.prototype = {
 5     sayColor: function() {
 6         //直接調用抽象方法
 7         this.initial();
 8     },
 9     //定義一個空的抽象方法由派生類去實現,也能夠不定義
10     initial: function() { }
11 }
12 
13 //ClassA做爲基類派生出ClassB
14 var ClassB = ClassA.extend(function(name) {
15     this.name = name;
16 }, {
17     //實現基類中的抽象方法
18     initial: function() {
19         console.log(this.name);
20     }
21 },
22 {
23     //無靜態成員
24 });

這樣的實現與真正意義上的多態相差有點大,可能會讓人疑惑這種必要性,爲了最大程度的知足嚴格意義上的多態,咱們改寫上面的代碼以下:

 1 //抽象類
 2 function ClassA() { throw new Error("can't instantiate abstract classes."); }
 3 ClassA.prototype = {
 4     initial: function() { throw new Error("can't call abstract methods."); }
 5 }
 6 
 7 //ClassA做爲基類派生出ClassB
 8 var ClassB = ClassA.extend(function(name) {
 9     this.name = name;
10 }, {
11     //實現基類中的抽象方法
12     initial: function() {
13         console.log(this.name);
14     }
15 },
16 {
17     //無靜態成員
18 });

爲了避免讓抽象類實例化,咱們直接在其構造函數中拋出異常,爲了避免能直接調用抽象方法,咱們也直接在其抽象方法中拋出異常,這樣咱們就知足了抽象類/方法的嚴格要求。

  至此,JavaScript中面向對象的實現就結束了,其類的實現也是一種模塊化,這樣代碼的可讀性就一步增強,具體在咱們的平常工做中,不多會能夠這樣封裝,也沒有必要,但在大型web應用中,用模塊化、抽象化來重構js代碼將顯得比較迫切,用面向對象去面對需求的多樣性,以最少的改動去知足新的需求,何樂而不爲,由於分享,因此快樂,在與你們交流中成長~~~

相關文章
相關標籤/搜索