JavaScript 物件導向程式設計(下)——繼承與多態

來源:互聯網
上載者:User

前面我們討論了如何在 JavaScript 語言中實現對私人執行個體成員、公有執行個體成員、私人靜態成員、公有靜態成員和靜態類的封裝。這次我們來討論一下物件導向程式設計中的另外兩個要素:繼承與多態。

1 又是幾個基本概念
為什麼要說又呢? 

在討論繼承時,我們已經列出了一些基本概念了,那些概念是跟封裝密切相關的概念,今天我們要討論的基本概念,主要是跟繼承與多態相關的,但是它們跟封裝也有一些聯絡。

1.1 定義和賦值
變數定義是指用

var a;
這種形式來聲明變數。

函數定義是指用

function a(...) {...}
這種形式來聲明函數。

var a = 1;
是兩個過程。第一個過程是定義變數 a,第二個過程是給變數 a 賦值。

同樣

var a = function(...) {};
也是兩個過程,第一個過程是定義變數 a 和一個匿名函數,第二個過程是把匿名函數賦值給變數 a。

變數定義和函數定義是在整個指令碼執行之前完成的,而變數賦值是在執行階段完成的。

變數定義的作用僅僅是給所聲明的變數指明它的範圍,變數定義並不給變數初始值,任何沒有定義的而直接使用的變數,或者定義但沒有賦值的變數,他們的值都是 undefined。

函數定義除了聲明函數所在的範圍外,同時還定義函數體結構。這個過程是遞迴的,也就是說,對函數體的定義包括了對函數體內的變數定義和函數定義。

通過下面這個例子我們可以更明確的理解這一點:

alert(a);
alert(b);
alert(c);
var a = "a";
function a() {}
function b() {}
var b = "b";
var c = "c";
var c = function() {}
alert(a);
alert(b);
alert(c);
猜猜這個程式執行的結果是什嗎?然後執行一下看看是不是跟你想的一樣,如果跟你想的一樣的話,那說明你已經理解上面所說的了。

這段程式的結果很有意思,雖然第一個 alert(a) 在最前面,但是你會發現它輸出的值竟然是 function a() {},這說明,函數定義確實在整個程式執行之前就已經完成了。

再來看 b,函數 b 定義在變數 b 之前,但是第一個 alert(b) 輸出的仍然是 function b() {},這說明,變數定義確實不對變數做什麼,僅僅是聲明它的範圍而已,它不會覆蓋函數定義。

最後看 c,第一個 alert(c) 輸出的是 undefined,這說明 var c = function() {} 不是對函數 c 定義,僅僅是定義一個變數 c 和一個匿名函數。

再來看第二個 alert(a),你會發現輸出的竟然是 a,這說明指派陳述式確實是在執行過程中完成的,因此,它覆蓋了函數 a 的定義。

第二個 alert(b) 當然也一樣,輸出的是 b,這說明不管指派陳述式寫在函數定義之前還是函數定義之後,對一個跟函數同名的變數賦值總會覆蓋函數定義。

第二個 alert(c) 輸出的是 function() {},這說明,指派陳述式是順序執行的,後面的賦值覆蓋了前面的賦值,不管賦的值是函數還是其它對象。

理解了上面所說的內容,我想你應該知道什麼時候該用 function x(..) {…},什麼時候該用 var x = function (…) {…} 了吧?

最後還要提醒一點,eval 中的如果出現變數定義和函數定義,則它們是在執行階段完成的。所以,不到萬不得已,不要用 eval!另外,即使要用 eval,也不要在裡面用局部變數和局部方法!

1.2 this 和執行內容
在前面討論封裝時,我們已經接觸過 this 了。在對封裝的討論中,我們看到的 this 都是表示 this 所在的類的執行個體化對象本身。真的是這樣嗎?

先看一下下面的例子吧:

var x = "I'm a global variable!";
function method() {
    alert(x);
    alert(this.x);
}
function class1() {
    // private field
    var x = "I'm a private variable!";
    // private method
    function method1() {
        alert(x);
        alert(this.x);
    }
    var method2 = method;
    // public field
    this.x = "I'm a object variable!";
    // public method
    this.method1 = function() {
        alert(x);
        alert(this.x);
    }
    this.method2 = method;
    // constructor
    {
        this.method1();     // I'm a private variable!
                            // I'm a object variable!
        this.method2();     // I'm a global variable!
                            // I'm a object variable!
        method1();          // I'm a private variable!
                            // I'm a global variable!
        method2();          // I'm a global variable!
                            // I'm a global variable!
        method1.call(this); // I'm a private variable!
                            // I'm a object variable!
        method2.call(this); // I'm a global variable!
                            // I'm a object variable!
    }
}
 
var o = new class1();
method();       // I'm a global variable!
                // I'm a global variable!
o.method1();    // I'm a private variable!
                // I'm a object variable!
o.method2();    // I'm a global variable!
                // I'm a object variable!

為什麼是這樣的結果呢?

那就先來看看什麼是執行內容吧。那什麼是執行內容呢?

如果當前正在執行的是一個方法,則執行內容就是該方法所附屬的對象,如果當前正在執行的是一個建立對象(就是通過 new 來建立)的過程,則建立的對象就是執行內容。

如果一個方法在執行時沒有明確的附屬於一個對象,則它的執行內容是全域對象(頂級對象),但它不一定附屬於全域對象。全域對象由當前環境來決定。在瀏覽器環境下,全域對象就是 window 對象。

定義在所有函數之外的全域變數和全域函數附屬於全域對象,定義在函數內的局部變數和局部函數不附屬於任何對象。

那執行內容跟變數範圍有沒有關係呢?

執行內容與變數範圍是不同的。

一個函數賦值給另一個變數時,這個函數的內部所使用的變數的範圍不會改變,但它的執行內容會變為這個變數所附屬的對象(如果這個變數有附屬對象的話)。

Function 原型上的 call 和 apply 方法可以改變執行內容,但是同樣不會改變變數範圍。

要理解上面這些話,其實只需要記住一點:

變數範圍是在定義時就確定的,它永遠不會變;而執行內容是在執行時才確定的,它隨時可以變。

這樣我們就不難理解上面那個例子了。this.method1() 這條語句(注意,這裡說的還沒有進入這個函數體)執行時,正在建立對象,那當前的執行內容就是這個正在建立的對象,所以 this 指向的也是當前正在建立的對象,在 this.method1() 這個方法執行時(這裡是指進入函數體),這個正在執行的方法所附屬的對象也是這個正在建立的對象,所以,它裡面 this.x 的 this 也是同一個對象,所以你看的輸出就是 I’m a object variable! 了。

而在執行 method1() 這個函數時(是指進入函數體後),method1() 沒有明確的附屬於一個對象,雖然它是定義在 class1 中的,但是他並沒有不是附屬於 class1 的,也不是附屬於 class1 執行個體化後的對象的,只是它的範圍被限制在了 class1 當中。因此,它的附屬對象實際上是全域對象,因此,當在它當中執行到 alert(this.x) 時,this.x 就成了我們在全域環境下定義的那個值為 “I’m a global variable!” 的 x 了。

method2() 雖然是在 class1 中定義的,但是 method() 是在 class1 之外定義的,method 被賦值給 method2 時,並沒有改變 method 的範圍,所以,在 method2 執行時,仍然是在 method 被定義的範圍內執行的,因此,你看到的就是兩個 I’m a global variable! 輸出了。同樣,this.method2() 調用時,alert(x) 輸出 I’m a global variable! 也是這個原因。

因為 call 會改變執行內容,所以通過 method1.call(this) 和 method2.call(this) 時,this.x 都變成了 I’m a object variable!。但是它不能改變範圍,所以 x 仍然跟不使用 call 方法調用時的結果是一樣的。

而我們後面執行 o.method1() 時,alert(x) 沒有用 this 指出 x 的執行內容,則 x 表示當前執行的函數所在的範圍中最近定義的變數,因此,這時輸出的就是 I’m a private variable!。最後輸出 I’m a object variable! 我想不用我說大家也知道為什麼了吧?:D

2 繼承和多態
2.1 從封裝開始
前面我們說了,封裝的目的是實現資料隱藏。

但是更深一層來說,在 javascript 中進行封裝還有以下幾個好處:

1、隱藏實現細節,當私人部分的實現完全重寫時,並不需要改變調用者的行為。這也是其它物件導向語言要實現封裝的主要目的。

2、javascript 中,局部變數和局部函數訪問速度更快,因此把私人欄位以局部變數來封裝,把私人方法以局部方法來封裝可以提高指令碼的執行效率。

3、對於 javascript 壓縮混淆器(據我所知,目前最好的 javascript 分析、壓縮、混淆器就是 JSA)來說,局部變數和局部函數名都是可以被替換的,而全域變數和全域函數名是不可以被替換的(實際上,對於 javascript 指令碼解析器工作時也是這樣的)。因此,不論對於開源還是非開源的 javascript 程式,當私人欄位和私人方法使用封裝技術後,編寫代碼時就可以給它們定義足夠長的表意名稱,增加代碼的可讀性,而發布時,它們可以被替換為一些很短的名稱(一般是單字元名稱),這樣就可以得到充分的壓縮和混淆。及減少了頻寬佔用,又可以真正實現細節的隱藏。

所以,封裝對於 javascript 來說,是非常有用的!

那麼在 javascript 實現繼承是為了什麼呢?

2.2 為什麼要繼承
在其它物件導向程式設計語言中,繼承除了可以減少重複代碼的編寫外,最大的用處就是為了實現多態。尤其是在強型別語言中,尤為如此:

1、在強型別語言中,一個變數不能夠被賦予不同類型的兩個值,除非這兩種類型與這個變數的類型是相容的,而這個相容的關係就是由繼承來實現的。

2、在強型別語言中,對一個已有的類型無法直接進行方法的擴充和改寫,要擴充一個類型,唯一的方法就是繼承它,在它的子類中進行擴充和改寫。

因此,對於強型別的物件導向語言,多態的實現是依賴於繼承的實現的。

而對於 javascript 語言來說,繼承對於實現多態則顯得不那麼重要:

1、在 javascript 語言中,一個變數可以被賦予任何類型的值,且可以用同樣的方式調用任何類型的對象上的同名方法。

2、在 javascript 語言中,可以對已有的類型通過原型直接進行方法的擴充和改寫。

所以,在 javascript 中,繼承的主要作用就是為了減少重複代碼的編寫。

接下來我們要談的兩種實現繼承的方法可能大家已經都很熟悉了,一種是原型繼承法,一種是調用繼承法,這兩種方法都不會產生副作用。我們主要討論的是這兩種方法的本質和需要注意的地方。

2.3 原型繼承法
在 javascript 中,每一個類(函數)都有一個原型,該原型上的成員在該類執行個體化時,會傳給該類的執行個體化對象。執行個體化的對象上沒有原型,但是它可以作為另一個類(函數)的原型,當以該對象為原型的類執行個體化時,該對象上的成員就會傳給以它為原型的類的執行個體化對象上。這就是原型繼承的本質。

原型繼承也是 javascript 中許多原生對象所使用的繼承方法。

function parentClass() {
    // private field
    var x = "I'm a parentClass field!";
    // private method
    function method1() {
        alert(x);
        alert("I'm a parentClass method!");
    }
    // public field
    this.x = "I'm a parentClass object field!";
    // public method
    this.method1 = function() {
        alert(x);
        alert(this.x);
        method1();
    }
}
parentClass.prototype.method = function () {
    alert("I'm a parentClass prototype method!");
}
parentClass.staticMethod = function () {
    alert("I'm a parentClass static method!");
}
 
function subClass() {
    // private field
    var x = "I'm a subClass field!";
    // private method
    function method2() {
        alert(x);
        alert("I'm a subClass method!");
    }
    // public field
    this.x = "I'm a subClass object field!";
    // public method
    this.method2 = function() {
        alert(x);
        alert(this.x);
        method2();
    }
    this.method3 = function() {
        method1();
    }
}
 
// inherit
subClass.prototype = new parentClass();
subClass.prototype.constructor = subClass;
 
// test
var o = new subClass();
 
alert(o instanceof parentClass);    // true
alert(o instanceof subClass);       // true
 
alert(o.constructor);  // function subClass() {...}
 
o.method1();    // I'm a parentClass field!
                // I'm a subClass object field!
                // I'm a parentClass field!
                // I'm a parentClass method!
o.method2();    // I'm a subClass field!
                // I'm a subClass object field!
                // I'm a subClass field!
                // I'm a subClass method!
o.method();     // I'm a parentClass prototype method!
 
o.method3();               // Error!!!
subClass.staticMethod();   // Error!!!

上面這個例子很好的反映出了如何利用原型繼承法來實現繼承。

利用原型繼承的關鍵有兩步操作:

首先建立一個父類的執行個體化對象,然後將該對象賦給子類的 prototype 屬性。

這樣,父類中的所有公有執行個體成員都會被子類繼承。並且用 instanceof 運算子判斷時,子類的執行個體化對象既屬於子類,也屬於父類。

然後將子類本身賦值給它的 prototype 的 constructor 屬性。(注意:這裡賦值的時候是沒有 () 的!)

這一步是為了保證在查看子類的執行個體化對象的 constructor 屬性時,看到的是子類的定義,而不是其父類的定義。

接下來,通過對 o.method1() 調用的結果我們會看到,子類繼承來的公有執行個體方法中,如果調用了私人執行個體欄位或者私人執行個體方法,則所調用的這些私人執行個體成員是屬於父類的。

同樣,通過對 o.method2() 調用的結果我們看到,子類中定義的執行個體方法,如果調用了私人執行個體欄位或者私人執行個體方法,則所調用的這些私人執行個體成員是屬於子類的。

通過對 o.method() 調用的結果我們看到,定義在父類原型上的方法,會被子類繼承。

通過對 o.method3() 調用的結果我們看到,子類中定義的執行個體方法是不能訪問父類中定義的私人執行個體成員的。

最後,通過對 subClass.staticMethod() 調用的結果我們看到,靜態成員是不會被繼承的。

2.4 調用繼承法
調用繼承的本質是,在子類的構造器中,讓父類的構造器方法在子類的執行內容上執行,父類構造器方法上所有通過 this 方式操作的內容實際上都都是操作的子類的執行個體化對象上的內容。因此,這種做法僅僅為了減少重複代碼的編寫。

function parentClass() {
    // private field
    var x = "I'm a parentClass field!";
    // private method
    function method1() {
        alert(x);
        alert("I'm a parentClass method!");
    }
    // public field
    this.x = "I'm a parentClass object field!";
    // public method
    this.method1 = function() {
        alert(x);
        alert(this.x);
        method1();
    }
}
parentClass.prototype.method = function () {
    alert("I'm a parentClass prototype method!");
}
 
parentClass.staticMethod = function () {
    alert("I'm a parentClass static method!");
}
 
function subClass() {
    // inherit
    parentClass.call(this);
 
    // private field
    var x = "I'm a subClass field!";
    // private method
    function method2() {
        alert(x);
        alert("I'm a subClass method!");
    }
    // public field
    this.x = "I'm a subClass object field!";
    // public method
    this.method2 = function() {
        alert(x);
        alert(this.x);
        method2();
    }
    this.method3 = function() {
        method1();
    }
}
 
// test
var o = new subClass();
 
alert(o instanceof parentClass);    // false
alert(o instanceof subClass);       // true
 
alert(o.constructor);  // function subClass() {...}
 
o.method1();    // I'm a parentClass field!
                // I'm a subClass object field!
                // I'm a parentClass field!
                // I'm a parentClass method!
o.method2();    // I'm a subClass field!
                // I'm a subClass object field!
                // I'm a subClass field!
                // I'm a subClass method!
 
o.method();                // Error!!!
o.method3();               // Error!!!
subClass.staticMethod();   // Error!!!

上面這個例子很好的反映出了如何利用調用繼承法來實現繼承。

利用調用繼承的關鍵只有一步操作:

就是在子類定義時,通過父類的 call 方法,將子類的 this 指標傳入。使父類方法在子類上下文中執行。

這樣,父類中的所有在父類內部通過 this 方式定義的公有執行個體成員都會被子類繼承。

用 instanceof 運算子判斷時,子類的執行個體化對象只屬於子類,不屬於父類。

查看子類的執行個體化對象的 constructor 屬性時,看到的是子類的定義,不是其父類的定義。

接下來,通過對 o.method1() 和 o.method2() 調用的結果跟原型繼承法的調用結果是相同的,所說明的問題也是一樣的,這裡不再重複。

通過對 o.method() 調用的結果我們看到,定義在父類原型上的方法,不會被子類繼承。

通過對 o.method3() 調用的結果我們看到,子類中定義的執行個體方法同樣不能訪問父類中定義的私人執行個體成員的。

最後,通過對 subClass.staticMethod() 調用的結果我們看到,靜態成員同樣不會被繼承的。

最後,還有一點,在這個例子中沒有體現出來,就是通過調用繼承法,可以實現多繼承。也就是說,一個子類可以從多個父類中繼承通過 this 方式定義在父類內部的所有公有執行個體成員。

作為一種弱類型語言,javascript 提供了豐富的多態性,javascript 的多態性是其它強型別物件導向語言所不能比的。

多態
重載和覆蓋
先來說明一下重載和覆蓋的區別。重載的英文是 overload,覆蓋的英文是 override。發現網上大多數人把 override 當成了重載,這個是不對的。重載和覆蓋是有區別的。

重載的意思是,同一個名字的函數(注意這裡包括函數)或方法可以有多個實現,他們依靠參數的類型和(或)參數的個數來區分識別。

而覆蓋的意思是,子類中可以定義與父類中同名,並且參數類型和個數也相同的方法,這些方法的定義後,在子類的執行個體化對象中,父類中繼承的這些同名方法將被隱藏。

重載
javascript 中函數的參數是沒有類型的,並且參數個數也是任意的,例如,儘管你可以定義一個:

function add(a, b) {
    return a + b;
}

這樣的函數,但是你仍然可以再調用它是帶入任意多個參數,當然,參數類型也是任意的。至於是否出錯,那是這個函數中所執行的內容來決定的,javascript 並不根據你指定的參數個數和參數類型來判斷你調用的是哪個函數。

因此,要定義重載方法,就不能像強型別語言中那樣做了。但是你仍然可以實現重載。就是通過函數的 arguments 屬性。例如:

function add() {
    var sum = 0;
    for (var i = 0; i < arguments.length; i++) {
        sum += arguments[i];
    }
    return sum;
}

這樣你就實現了任意多個參數加法函數的重載了。

當然,你還可以在函數中通過 instanceof 或者 constructor 來判斷每個參數的類型,來決定後面執行什麼操作,實現更為複雜的函數或方法重載。總之,javascript 的重載,是在函數中由使用者自己通過操作 arguments 這個屬性來實現的。

覆蓋
實現覆蓋也很容易,例如:

function parentClass() {
    this.method = function() {
        alert("parentClass method");
    }
}
function subClass() {
    this.method = function() {
        alert("subClass method");
    }
}
subClass.prototype = new parentClass();
subClass.prototype.constructor = subClass;
 
var o = new subClass();
o.method();

這樣,子類中定義的 method 就覆蓋了從父類中繼承來的 method 方法了。

你可能會說,這樣子覆蓋是不錯,但 java 中,覆蓋的方法裡面可以調用被覆蓋的方法(父類的方法),在這裡怎麼實現呢?也很容易,而且比 java 中還要靈活,java 中限制,你只能在覆蓋被覆蓋方法的方法中才能使用 super 來調用次被覆蓋的方法。我們不但可以實現這點,而且還可以讓子類中所有的方法中都可以調用父類中被覆蓋的方法。看下面的例子:

function parentClass() {
    this.method = function() {
        alert("parentClass method");
    }
}
function subClass() {
    var method = this.method;
    this.method = function() {
        method.call(this);
        alert("subClass method");
    }
}
subClass.prototype = new parentClass();
subClass.prototype.constructor = subClass;
 
var o = new subClass();
o.method();

你會發現,原來這麼簡單,只要在定義覆蓋方法前,定義一個私人變數,然後把父類中定義的將要被覆蓋的方法賦給它,然後我們就可以在後面繼續調用它了,而且這個是這個方法是私人的,對於子類的對象是不可見的。這樣跟其它進階語言實現的覆蓋就一致了。

最後需要注意,我們在覆蓋方法中調用這個方法時,需要用 call 方法來改變執行內容為 this(雖然在這個例子中沒有必要),如果直接調用這個方法,執行內容就會變成全域對象了。

 

 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.