Js中的原型繼承和原型鏈

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/DoubleJan/article/details/53616755

第一次接觸Javascript的時候是爲了寫一個前端頁面,當時用了各種DOM方法,後來知道了Jquery,Js代碼寫起來更加方便,曾經以爲Js不過如此,直到最近開始學習Js語言的時候,才發現以前看到的都是冰山一角,真正的Js語言本身更加複雜,更加龐大,更加有趣

今天我主要討論Js中面向對象概念中的十分重要且核心的思想,原型和原型鏈。

Js是面向對象的語言,但是卻不像C++那樣有嚴謹的類,和封裝結構,甚至,連接口繼承也沒有。那麼Js是如何實現對象的呢,就是用原型的思想。

首先我們前面說過,Js中的函數是一種類型(Function),也是一種對象。函數名是一個指針。嚴格來說,Js中沒有構造函數的概念,但是可以一般的函數完成構造函數的功能

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["Shelby","Court"];
}
var person1=new Person("Nicholas",29,"software Engineer");
var person2=new Person("Greg",27,"Docker");

以上的函數確實可以起到構造函數的作用,之前說過,函數實際上也是對象,那麼他就也會有this指針指向本體。實際上,Person函數每調用一次就會新建一個實例,並且包含一些屬性。但是如果在函數中加上一行

this.sayName=function(){
alert(this.name);
}

函數變成這樣:

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["Shelby","Court"];
    this.sayName=function(){
        alert(this.name);
    }
}

那麼你將會發現,每次調用構造函數,不僅會生成新的屬性,也會重新定義方法sayName,這違背了代碼複用的原則,這就是工廠模式的不足。

還有一種是原型模式,因爲每創建一個函數,會自動生成一個原型對象,注意,是任何一個函數都會,這個對象負責包含一切屬性和方法。原型模式,和更好的組合模式,寄生原型模式,都是基於這樣的機制。

函數的原型可以通過函數的prototype屬性訪問。所以我們可以有下面的代碼:

function Person(){

}

Person.prototype.name="Nicholas";
Person.prototype.age=29;
Person.prototype.job="software Engineer";
Person.prototype.sayName=function(){
    alert(this.name);
};

上面的代碼,創建了一個function Person()函數,作爲函數,他什麼都不做,我們更關心是創建這個函數之後隱藏的原型對象,我們通過Person.prototype屬性訪問原型對象,實際上,這裏的Person.prototype就是函數原型對象本身,因爲prototype是一個指針,指向原型對象,並且對原型對象創建name等屬性並賦值,並且創建了sayName方法,Person.prototype.name會自動創建name屬性,如果沒有的話,如果函數中有name屬性,則會按照賦值語句更新name的值。Js的寫法有時候就是這麼隨意。

但是,這種原型模式也有一個缺點,我們定義了一個沒有功能的函數,就爲了他背後的原型,這增加了代碼的冗雜,並且,由於每次調用函數,實際上並不做什麼,因爲無論函數被調用幾次,原型對象只會有一個,所以,所有的通過調用Person生成的對象實例,共享原型對象Person.prototype的所有數據。然而,按照一般面向對象的思想,我們希望我們的數據是每個實例所私有,而共享的是共有方法。因爲創造一大批完全一樣的東西是沒有任何必要的。

所以我們可以嘗試將屬性定義在構造函數中,方法定義在原型對象中,這就是組合模式:

function Person(name,age,job){
    this.name=name;
    this.age=age;
    this.job=job;
    this.friends=["Shelby","Court"];
}

Person.prototype={
    constructor:Person,
    sayName:function(){
        alert(this.name);
    }
}

var person1=new Person("Nicholas",29,"software Engineer");
var person2=new Person("Greg",27,"Docker");

person1.friends.push("van");

alert(person1.friends);
alert(person2.friends);
alert(person1.friends===person2.friends);
alert(person2.sayName===person2.sayName);

以上代碼中,把所有的私有屬性放在了構造函數中,因此每次調用構造函數都會新建一個實例,幷包含這些屬性的一個獨立副本,而所有處理數據的方法都包含在唯一一個原型對象中,實現了代碼的複用。因此這是一種相對穩妥的實現方法,但是在繼承時,也會遇到一些問題。

我們用這種思路實現繼承:

function SuperType(name){
    this.name=name;
    this.colors=["red","blue","green"];
}

SuperType.prototype.sayName=function (){
    alert(this.name);
};

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}

SubType.prototype=new SuperType();
SubType.prototype.constructor=SubType;
SubType.prototype.sayAge=function(){
    alert(this.age);
}

var instance1=new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors);
instance1.sayName();
instance1.sayAge();

var instance2=new SubType("Greg",27);
// alert(instance2.colors);
instance2.sayName();
instance2.sayAge();

alert(Object.prototype.isPrototypeOf(SubType));

alert(SubType.prototype.constructor==SuperType);

alert(Object.prototype.isPrototypeOf(SuperType.prototype));

上面代碼中,首先定義了一個SuperType()作爲父對象,構造函數內部放了屬性,在外部重寫了原型,添加了sayName方法。
之後定義了一個SubType()對象,用於繼承SuperType(),這體現在兩個方面:
首先,SubType的構造函數中通過調用call方法,調用了SuperType的構造函數,也就是說,每次調用SubType的構造函數時都會在SubType中新建一個SuperType對象,使得SubType擁有SuperType的屬性,
其次,體現在後面的用SuperType()重寫SubType.prototype,這使得SubType繼承SuperType的方法。

至此,我們似乎完美實現了對數據的封裝和繼承,但是還有很多隱藏的問題。

首先,組合模式遇到繼承後,我們從構造函數中繼承了屬性,放到了子對象的構造函數中,但是,當我們用下面的代碼重寫原型時:

SubType.prototype=new SuperType();

此時,會用SuperType對象重寫SubType原型對象,這實際上相當於把一個SuperType的實例放到SubType.prototype中,這意味着,SuperType的所有數據,包括構造函數中存放的私有屬性,和SuperType原型中存放的公有方法,都會寫入SubType的原型中,換句話說,我們不僅在SubType.prototype中保存了一份SuperType的屬性,也在SubType中保存了一份(因爲在函數內調用他的構造函數),雖然實例中的屬性會屏蔽掉原型中的屬性,因此代碼運行起來並不會出現什麼問題,但是,實際上卻造成了資源的浪費。

因此我們更加完善的方法是寄生組合式繼承:

function SuperType(name){
    this.name=name;
    this.colors=["red","blue","green"];
}

SuperType.prototype.sayName=function(){
    alert(this.name);
}

function SubType(name,age){
    SuperType.call(this,name);
    this.age=age;
}

function inheritPrototype(sub,super){
    var prototype=Object(super.prototype);
    prototype.constructor=sub;
    sub.prototype=prototype;

}

inheritPrototype(SubType,SuperType);

SubType.prototype.sayAge=function (){
    alert(this.age);
}

上述代碼中核心是:

function inheritPrototype(sub,super){
    var prototype=Object(super.prototype);
    prototype.constructor=sub;
    sub.prototype=prototype;

}

該函數接受兩個參數,第一個是子對象,第二個是父對象
我們分析一下我們的需求,我們在繼承時,實際上我們通過在子對象的構造函數中調用父對象的構造函數,實際上已經繼承了父對象的所有屬性,因此,在原型中只是需要父對象的原型數據,並且,前面我們說過,所有的對象,包括函數,實際上都是Object對象的一種繼承,因此,實際上我們新建的所有對象和函數本質上都是object對象,因此,我們可以將SuperType的原型(SuperType.prototype)作爲參數新建一個Object對象實例,並且複製給一個變量,那麼,。這個變量中將只有父元素的原型對象中的數據。
然後將子對象的原型重寫爲父對象的原型,這樣實際上是吧父元素原型對象的數據複製到了子對象原型中。

然後我們注意到了代碼中還有一句:

prototype.constructor=sub;

這句話實際上可以沒有,但是無論是邏輯上還是習慣上,我們都要加上這麼個賦值語句。
原因是,繼承,這一行爲,會打亂對象原型鏈

什麼事對象的原型鏈,這類似於我們數據結構中的雙向鏈表。雙向鏈表中除了頭指針和尾指針,以外,每個元素都有兩個指針用於定位,一個指向上一元素,一個指向下一元素。那麼Js中的對象也有類似的行爲,即,prototype屬性和constructor屬性,這兩個屬性本質上是指針,屬於Object對象的屬性,由於所有的對象都是Object對象的子對象,所以所有的對象都有這兩個屬性,並且函數實際上也是一個對象,所以,每一個函數中也有這兩個指針。
其中,對於構造函數來說,prototype屬性默認指向隱藏的原型對象,原型對象中的constructor屬性回指構造函數。
在默認情況下,構造函數的prototype屬性只想原型對象,原型對象的constructor回指構造函數,原型對象的prototype指向Object。這就形成一個閉環,被稱爲原型鏈
但是這只是理想情況,這兩個指針可以幫助我們尋找之間的關係,但是,本質上只是兩個指針,隨時可以被賦值和更改,實際上,也經常被重寫。

每次我們重寫一次原型對象,這兩個指針都會被重寫,大多數情況下,這兩個屬性會指向Object對象:

SubType.prototype=new SuperType();

這一句代碼使得SubType的原型被重寫,此時,prototype和constructor都會指向SuperType,但是很顯然,這不僅是不合邏輯的,並且是浪費的,因爲一般情況constructor要回指構造函數,並且,沒有必要兩個指針指向同一個值,所以,我們加了一句:

SubType.prototype.constructor=SubType;

這使SubType原型的constructor正常工作.
同樣的,在使用寄生組合式繼承時:

prototype.constructor=sub

這樣將使得super的原型的constructor指向Sub的構造函數。這樣在邏輯上是比較合理的,並且不會浪費一個指針。

並且,由於每次調用構造函數,新建實例化對象後,都會重寫原型,所以,subType和SuperType的構造函數的prototype都指向了Object.prototype,我們可以使用Object.setPrototypeOf來重寫prototype的指向,注意,這個方法的調用對象始終是Object,它接受兩個參數,第一個是要修改的prototype,第二個是新的指向:

Object.setPrototypeOf(SubType,SubType.prototype);

這將使SubType的構造函數重新指向原型,同理,這在邏輯上是比較合理的。

綜上,我們發現,原型根據內部機制決定繼承關係,而不是prototype和constructor的指向,這兩個屬性實際上只是爲了方便查找原型之間關係,理解原型鏈的,雖然 ,事實上它的低信任度使得這個功能並沒卵用。

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