深入理解javascript中的繼承機制(1)

chi633發表於2017-12-21

javascript中的繼承機制是建立在原型的基礎上的,所以必須先對原型有深刻的理解,筆者在之前已經寫過關於js原型的文章。

我們都知道,每個函式function都會有一個屬性,這個屬性就是原型prototype,它是一個引用,它指向一個物件object。當我們使用new操作符呼叫建構函式,建立一個新物件的時候,這個新物件就會擁有一個指向它的建構函式的原型物件的神祕連結,在瀏覽器中一般是__proto__,通常我們也稱為它的原型物件。這就可以理解為,new出來的物件繼承擁有了了它的建構函式的原型物件,這就隱約有一點繼承的概念了。

#原型鏈繼承機制

原型鏈的概念就是多個這樣的物件通過proto相互關係起來

Paste_Image.png

上圖可以看到,物件A有一系列的屬性,但其中的部分屬性是藏在原型之中的,而這個原型又指向了物件B,所以物件A間接也擁有了物件B的部分屬性,同理,物件B的部分屬性是藏在proto中,而proto又指向了物件C. 這樣一環一環的往上遞加,顯然最後會指向Object.prototype,它是所有物件的父物件。

下面我們就通過一個例項來說明,原型鏈繼承機制的實現與原理 我們有三個建構函式,Shape,2DShape, Triangle。 很顯然,它們之間的繼承關係,應該是Shape,2DShape,Triangle. 下面分別定義三個建構函式:

function Shape(){
    this.name = 'Shape';
    this.toString = function () {
        return this.name;
    };
}

function TwoDShape(){
    this.name = '2D shape';
}

function Triangle(side, height){
    this.name = 'Triangle';
    this.side = side;
    this.height = height;
    this.getArea = function () {
        return this.side * this.height / 2;
    };
}
複製程式碼

接下來實現它們之間的原型鏈繼承關係:

TwoDShape.prototype = new Shape();
Triangle.prototype = new TwoDShape();
複製程式碼

當我們覆蓋原型物件的時候,不要忘了原型的陷阱(讀者若不清楚,可以參考筆者介紹原型的博文)

TwoDShape.prototype.constructor = TwoDShape;
Triangle.prototype.constructor = Triangle;
複製程式碼

這樣我們就實現了原型鏈的繼承關係。 每一個new出來的TwoDShape物件的proto屬性都指向一個Shape物件,所以它可以擁有Shape物件的屬性和方法,同理,每一個new出來的Triangle物件的proto屬性都指向一個TwoDShape物件,而TwoDShape又繼承至Shape,所以這樣就形成了一個原型鏈。

下面我們對以上原型鏈關係進行測試

Paste_Image.png

上圖我們可以看到清晰的一個原型鏈關係。

Paste_Image.png

我們看到實現原型鏈繼承關係之後,my作為子物件,同時都是父物件的一種,這是符合java等語言中繼承的概念的。

將共有的屬性放進原型中

如上個例子中的,name屬性是三中物件共有的,上個例子每個單獨的物件都會new出一個name屬性,這樣就造成了對空間的浪費。 所以我們將name屬性移到原型中去

function Shape() {}
Shape.prototype.name = 'Shape';
複製程式碼

就不用每次都new出一個name屬性,而是共用原型屬性裡面的name屬性。

下面我們就用這種思想完善之前的例子

// constructor
function Shape() {}
// augment prototype
Shape.prototype.name = 'Shape';
Shape.prototype.toString = function () {
    return this.name;
};
// another constructor
function TwoDShape() {}
// take care of inheritance
TwoDShape.prototype = new Shape();
TwoDShape.prototype.constructor = TwoDShape;
// augment prototype
TwoDShape.prototype.name = '2D shape';
function Triangle(side, height) {
    this.side = side;
    this.height = height;
}
// take care of inheritance
Triangle.prototype = new TwoDShape();
Triangle.prototype.constructor = Triangle;
// augment prototype
Triangle.prototype.name = 'Triangle';
Triangle.prototype.getArea = function () {
    return this.side * this.height / 2;
};
複製程式碼

Paste_Image.png

將部分共享屬性移到原型裡去之後,原型鏈的繼承關係如圖,對比之前簡潔了一些,因為沒有多餘的重複屬性

Paste_Image.png

這裡呼叫toString方法得到相同的結果,但與之前略有不同,這裡要多搜尋一次,因為toString方法是屬於Shape的原型屬性裡的。於是效率就有所降低。

同時,這種模式還有一個缺陷看下面的例子

Paste_Image.png

我們訪問Shape物件的name屬性結果顯示的確實Triangle,這是為什麼呢? 其實很簡單,因為我們所有的原型都指向同一個物件,而每個物件的原型屬性只是取得了指向唯一的原型物件的指標,所以只要改變了它,所有的都會改變了 因為這句: Triangle.prototype.name = 'Triangle'; 所以會導致Shape,TwoDShape的name屬性都都為Triangle。

所以在某些時候,就沒法使用這種繼承模式,這種將共享的屬性移到原型中的模式,會產生子物件覆蓋掉父物件共有屬性的缺陷。

相關文章