Js中的構造函數、原型、原型鏈、繼承

寫在前頭
首先思考一個問題,我們知道通過構造函數和new操作符可以創建一個新的對象,並且同一個構造函數可以創建出相同屬性和方法的對象,這正是prototype所做的(使用原型對象的好處是可以讓所有實例共享它所包含的屬性和方法)。並且,在其他構造函數中通過call和apply調用其他構造函數不就可以實現繼承了嗎,那我們還要prototype做什麼?

原因是,其實new操作符生成的對象並不能共享屬性和方法,每次new一個新對象時,都要爲這個對象開闢一個新的空間來存放它的屬性和方法。而且,在構造函數中,每次想要修改某個屬性和方法時,都要重新生成所有的實例,對資源造成了極大的浪費。因爲不想浪費,所以就引入了原型。

理解原型對象
遵循以下幾個原則:
1:我們創建的每個函數都有一個prototype屬性,這個屬性是個指針,指向一個對象(這個對象就是原型對象),而這個對象包含了所有實例共享的屬性和方法。
2:原型對象除了1所說包含所有實例的共享的屬性和方法,還有一個constructor指針指向構造函數,對的,構造函數通過constructor和prototype互指。

function animal(){}
animal.prototype.name = 'Tom';
animal.prototype.say = function(){
	alert('喵喵');
}

var cat = new animal();
console.log(cat.name) //"Tom"
console.log(cat.say) //"喵喵喵?"

ok,解釋一下。上面代碼中
1, 構造函數爲aniaml,當我們創建了這個構造函數時,他已經包含了一個原型屬性
prototype
2, 該構造函數的原型對象
console.log(animal.prototype) //返回一個對象,即原型對象,該對象包含一個name屬性,一個say函數
3, 該實例
cat.__prpto__ == animal.prototype //true

在這個例子中,構造函數自身爲空,在構造函數的原型對象上創建了一個屬性和方法,他們被所有實例所共享,因爲實例中有proto指針指向了該原型對象。但是如果使用這種方法,我們在構造函數內寫的屬性和方法不是都可以轉到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");
var person2 = new Person("Greg",27,"Doctor");

perons1.frineds.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //“Shelby,Count”

在這個例子中,構造函數中定義實例屬性,原型對象上定義方法和共享屬性。每次調用構造函數,都會給新的對象開闢一個新的空間,此時,實例上的friends是全新的,非proto指向,擁有獨立的空間,所以每個空間的friends數組是單獨互不影響的,因此在person1對象中添加一個friends後,person2的friends屬性並未改變,而person1和person2都可以共享sayName函數。

原型鏈繼承
在java等oop語言中繼承由class和extend關鍵字來實現,而在ES5中沒有這些關鍵字( ES6有 ),因此,只能通過使子函數的原型對象等於父函數的實例來實現鏈式繼承。

function SuperType(){
    this.color = ['red', 'blue' , 'yellow'];
}

function SubType(){}

SubType.prototype = new SuperType(); //子函數的原型對象等於父函數的實例

var instance1 = new SubType();
instance1.color.push ('white');
alert(instance1.color); // red,blue,yellow,white

var instance2 = new SubType();
alert(instance2.color); // red,blue,yellow,white

子函數的原型對象等於父函數的實例,此時子函數SubType的原型被改寫,成爲了SuperType的實例,而上面講了實例擁有一個proto屬性(指針)指向原型對象,因此這個SubType的原型對象上的proto屬性指向了SuperType的原型對象。然後當我們創建了SubType的實例instance時,instance中也有一個proto指針,而它指向SubType.prototype,SubType.prototype上的proto指針,指向SuperType.prototype,其實再往上,SuperType.prototype也是對象的一種,因此它也是Object的一個實例,SuperType.prototype的proto指針指向Object.prototype。(頂層)(原型鏈的核心就是proto指針)。這種實例與原型的鏈條,就叫做原型鏈。

組合繼承
上述的SubType.prototype = new SuperType(); //子函數的原型對象等於父函數的實例的確實現了繼承,但是當SuperType構造函數中有引用型數據時,即此時SubType.prototype上也有了引用類型,現在又出現了上面提過的問題,在sub的實例中改變引用類型的值會反映到所有實例中。
第二點,在創建子類型的實例時無法給超類型的構造函數傳遞參數。

單獨使用借用構造函數
解決上述問題有一種借用構造函數的方法,在子構造函數中使用call或apply,如SuperType.call(this,arguments)改變sub構造函數中this的指向,而且可以通過這種方法向超類型傳遞參數,但是單單使用借用構造函數,並不能達到函數複用的效果,依然要爲每個實例上的函數開闢空間(因爲此時函數沒有定義在原型上),爲了融合借用構造函數和原型鏈繼承的優點,出現了組合繼承。

組合

function SuperType(name){
    this.name = name;
    this.color = ['red', 'blue' , 'yellow'];
}

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

function SubType(name, age){
    //繼承屬性 超類
    //獲得name和color屬性
    SuperType.call(this, name);
    this.age= age;
} 

//繼承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function(){
    alert(this.age)
}

var instance1 = new SubType("Nicholas", 29);
instance1.color.push("black");
console.log(instance1.color); //["red", "blue", "yellow", "black"]
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("Greg", 27);
console.log(instance2.color); //["red", "blue", "yellow"]
instance2.sayName();
instance2.sayAge();

在這個例子中,超類型構造函數定義了name和colors兩個屬性,超類型原型對象上定義了一個sayName函數可複用,子類型中使用借用構造函數的方法,使所有的子類實例都繼承了父類的colors和name屬性,還設立了一個自己的age屬性,之後使子類型的原型對象等於超類型的一個實例,繼承鏈生效,又在子類型的原型對象上增加了一個sayAge函數(此處順序不能反,否則子類原型對象被覆蓋,新函數無效),再之後新建了兩個子類型的實例,在instance1改變colors數組並未影響instance2,且子類型,超類型函數皆可調用。

此時的邏輯關係從所截的圖上來看,實例之所以引用類型互不影響,因爲此時實例中的colors屬性是同過借用構造函數,新建實例時,向超類型傳遞參數,並獲得了name和colors屬性,每個新的實例都是新的空間,且在原型鏈的最下面(原型鏈向上查找是否擁有某屬性),所以對數組的操作並不互相影響。
這裏子類型的實例其實若本身沒有colors屬性,指向原型的color屬性還是會互相影響的,所以是借用構造函數解決了這個問題,讓子類型實例先獲得了這個屬性,當在實例上訪問到時,便不會再往下尋找
子類型的實例上的proto依然指向子類型的原型對象,即SubType.prototype,其上還有一個定義的sayAge函數。
此原型對象因爲等於超類型的實例,所以也有一個proto指向超類型的原型對象,即SuperType.prototype。
其上還有一個sayName函數,SuperType.prototype上還有一個proto,再往下就是原生的Object.prototype了。

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