JavaScript的OO思想(二)

我們知道面向對象的語言有四大基本特性:抽象、繼承、封裝和多態。抽象性是指將具有一致的數據結構(屬性)和行爲(操作)的對象抽象成類。一個類就是這樣一種抽象,它反映了與應用有關的重要性質,而忽略其他一些無關內容。任何類的劃分都是主觀的,但必須與具體的應用有關。在C#等一些面嚮對象語言中有抽象類的概念,繼承了抽象類的類必須實現抽象類中的方法,而在JS中由於不存在類的概念,不存在重寫等關鍵字,所以JS中是沒有抽象概念。而JS是弱類型的語言,任何一個類型都可以賦值給任一屬性,所以我們可以說JS是支持多態的。所以總的來說JS支持繼承,封裝和多態。前面講到了對象的創建,可以理解爲封裝,這次我們主要說說這個繼承的思想。

與其他面嚮對象語言不同,JS中的繼承也有着自己獨特的實現方式。基本上有以下6中實現方式:原型鏈,借用構造函數,組合繼承,原型式繼承,寄生式繼承,寄生組合式繼承

1、原型鏈繼承
所謂的原型鏈繼承即是通過將父對象直接賦值給子對象的原型,這一樣子對象可以通過原型訪問父對象中的屬性和方法。我們知道,在創建一個新的對象時,原型指針都會默認的指向一個Object對象,因此我們可以通過原型指針訪問object中的方法,比如toString方法,我們可以說該對象實際上是通過原型鏈繼承了Object對象。實現原型鏈的繼承方式如以下所示:

function SuperType(){
    this.name = "I'm super type";
}

SuperType.prototype.sayHi = function(first_argument) {
    alert("Hi");
};

function SubType(){

}

SubType.prototype = new SuperType();

var sub = new SubType();
sub.sayHi();//Hi
alert(sub.name);//I'm super type
alert(sub.constructor == SuperType);//true

通過上面的原型鏈繼承的例子我們不難方面以下幾點:

1、和C#,Java等面嚮對象語言不同,它並不是繼承類的機構的方式,而是將一個父對象實例賦值給子對象的原型指針而實現繼承的
2、使用該方式的繼承不僅會繼承來自實例中的方法,而且會繼承其屬性,並且該屬性的可以說是靜態的,即在一個子對象的實例中改變,則其它實例中也會改變。
3、我們知道默認的原型對象裏會有一個constructor屬性,該屬性指向的是函數的本身,而現在整個原型對象被重寫了,子對象的構造函數屬性指向了父對象的函數,即現在sub的constructor指向的是SuperType函數。

2、借用構造函數
使用原型鏈可以實現繼承,但是這樣的繼承並不是完美的,大部分時候我們並不希望繼承的屬性是存在原型對象中的,因爲這樣帶來的負面效果是牽一髮而動全身,改變一處而處處改變,而這樣的實現是無法滿足一些特定的需求的。所以如果我們要子對象繼承父對象中的屬性,而又希望每個子對象中的屬性是獨立的話,則必須要考慮其他的實現方式。在此引申出另一種實現方法:借用構造函數。這種方式可以解決屬性繼承的問題。

function SuperType(){
    this.name = "Li Lei";
    this.sayHi = function() {
        alert("Hi");
    };
}

SuperType.prototype.show = function() {
    alert("Hello world");
};

function SubType(){
    SuperType.call(this);
}

var sub = new SubType();
sub.sayHi();//Hi
alert(sub.name);//Li Lei
alert(sub.constructor == SubType);//true
alert(sub.show);//undefined

我們可以看到實現這種繼承的方式只是在子類型的構造函數的內部調用父類型的構造函數而已。能實現這種方式的主要原因是函數是在特定環境中執行代碼的對象,因此通過使用apply()和call()方法也可以在新創建的對象上執行構造函數。淺顯點來說,現在做的事不過是將子對象當前作用域賦值給函數SuperType,然後執行SuperType函數,因爲此時SuperType函數裏的this爲sub,所以纔會使得sub有了name和sayHi屬性。
這種實現方式有以下幾點也需要注意:

1、該實現沒有改變原型對象
2、原型對象中的constructor指向的仍是子對象函數類型本身
3、父類型中的原型對象對子類型是不可見的

所以當我們想子類型能夠繼承父類型的原型時,借用構造函數方式是不能滿足需求的。

3、組合繼承
顧名思義組合繼承就是原型鏈+借用構造函數繼承了,它有時候也叫經典僞繼承。其思路是使用原型鏈繼承父類型的原型屬性和方法,使用借用構造函數來實現對實例屬性和方法的繼承。話不多說,看一段代碼。

function SuperType(){
    this.name = "Li Lei";
}
SuperType.prototype.show = function() {
    alert("Hello world");
};

function SubType(){
    SuperType.call(this);
}
SubType.prototype = new SuperType();

var sub = new SubType();
alert(sub.name);//Li Lei
alert(sub.constructor == SuperType);//false
sub.show();//Hello world

在這裏雖然SubType的原型中雖然也存在屬性name,但是由於利用借用構造函數方式,在構造函數內部又定義了name,此時這個那麼是覆蓋了原型中的name的。由此可見,組合繼承融合了原型鏈和借用構造函數的優點,避免了各自產生的缺陷。但是組合繼承也並非是完美的,它調用了兩次父類型的構造函數,可能會產生冗餘的方法和屬性,例如上面的例子中,原型對象和實例對象都有name屬性。

4、原型式繼承

如果你不想創建一個構造函數,而只是想讓一個對象和另一個對象相似的話,就可以使用原型式繼承了。

function create(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var people = {
    name: "Li Lei",
    friends: ["Han Meimei", "Jim Green"]
}

var tom = create(people);
tom.name = "Tom";
tom.friends.push("Li Lei");

var lucy = create(people);
lucy.name = "Lucy";
lucy.friends.push("Tom");

alert(people.friends);//Han Meimei,Jim Green,Li Lei,Tom

在ECMAScript5中通過新增了Object.create()的方法規範了原型式繼承,該方法接受兩個參數,一個是用作新對象原型的對象,一個是爲新對象定義額外屬性的對象,在只傳入一個對象的情況下,該方法和上面示例中的方法create中的表現是一致的。

var people = {
    name: "Li Lei",
    friends: ["Han Meimei", "Jim Green"]
}

var tom = Object.create(people);
tom.name = "Tom";
tom.friends.push("Li Lei");

var lucy = Object.create(people);
lucy.name = "Lucy";
lucy.friends.push("Tom");

alert(people.friends);//Han Meimei,Jim Green,Li Lei,Tom

5、寄生式繼承
這是一種與原型式繼承緊密相關的繼承,與寄生構造模式和工廠模式類似。相當於是加強版的原型式繼承。

function create(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

function createParasitic(obj){
    var para = create(obj);
    para.sayHi = function(){
        alert("Hi");
    }
    return para;
}

var people = {
    name: "Li Lei",
    friends: ["Han Meimei", "Jim Green"]
}

var lily = createParasitic(people);
lily.sayHi();// Hi

這種做法的結果是不僅得到了一個與另外一個對象相似的對象,而且在這個對象上添加了自己想要的方法。

6、寄生組合式繼承
前面我們說了組合繼承,它是JavaScript中最常用的模式,而組合繼承的不足就是調用了兩次構造函數,結果是產生了冗餘的屬性或者方法。

function SuperType(){
    this.name = "Li Lei";
}
SuperType.prototype.show = function() {
    alert("Hello world");
};

function SubType(){
    SuperType.call(this); //第二次調用構造函數
}
SubType.prototype = new SuperType(); //第一次調用構造函數

var sub = new SubType();
alert(sub.name);//"Li Lei"
alert(sub.constructor == SuperType);//true
sub.show();//Hello world

所謂的寄生組合式,指的是使用借用構造函數的方式來繼承屬性,使用原型鏈混合的方式來繼承方法。它的主題思想就是不必爲了子類型的原型而調用父類型的構造函數,我們所需要的只是父類型的原型對象的一個副本而已。其實現方式如下:

function create(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

function inheritPrototype(subType, superType){
    var proto = create(superType.prototype);
    proto.constructor = subType;
    subType.prototype = proto;
}

下面我們再來看一個使用寄生組合繼承的例子。

function create(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

function inheritPrototype(subType, superType){
    var proto = create(superType.prototype);
    proto.constructor = subType;
    subType.prototype = proto;
}

function SuperType(){
    this.name = "Li Lei";
}
SuperType.prototype.show = function() {
    alert("Hello world");
};

function SubType(){
    SuperType.call(this); //第二次調用構造函數
}

inheritPrototype(SubType, SuperType);

var sub = new SubType();
alert(sub.name);//"Li Lei"
alert(sub.constructor == SubType);//true
sub.show();//Hello world

至此,基本的繼承方式已經說完,每一種方式都有利有弊,關鍵在於用在合適的地方,才能發揮無限的威力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章