JavaScript原型鏈與繼承

QQ羣招募中646258285(招募中,沒幾個人說話),
需要交流的朋友可以直接加我微信( DntBeliv )或QQ( 1121864253 )


理解JavaScript函數與原型

在這裏最重要的是理解一點,JavaScript裏面一切都是一個值,換句話說,object是一個值,function也是一個值。這些值與基本類型的區別就在於,他們只是一個變量標籤,存儲的是一個指向其內容的指針。

函數相對來說就稍微複雜一點,因爲默認情況下函數沒有確定的運行環境,需要調用者提供運行環境(this)。但實際上函數本身就是一個變量,也是一個值,一個變量標籤,這就是函數對象。通過函數對象可以構造函數對象的實例,本質上是通過一定規則對函數對象的一個拷貝(可以先不理解這句話)。

先不管函數對象,使用函數分兩種情況,作爲普通函數而言,它只是一系列過程的封裝,我們使用的也只是這一系列的過程,也因此沒有體現出函數的面向對象特性,此時函數的運行環境是調用者,在全局環境下是全局對象。作爲構造函數而言,即new function(),其中function()裏面一般沒有返回值。它除了封裝了一系列方法,還將自身實例化爲一個對象實例,自己爲自己提供了運行環境。我們看下面這個例子

在這裏插入圖片描述

如果函數內部有返回值,使用函數時前面加個new,會怎麼樣呢?事實上,當我們自定義返回值時,則我們返回的值取代了函數默認生成的對象實例。因此返回的將是return後面的值。

現在來說原型,說到原型時,我們就要考慮到函數對象了。函數是一個對象,是一個值,除了自身的“構造方法”之外,實際上還會有一個默認的屬性:對於JavaScript而言,只要創建了一個新函數,就會根據一組特定的規則爲該函數創建一個prototype屬性,這個屬性是一個引用,其指向函數的原型對象。此外,這個原型對象又會有一個屬性constructor,也是一個引用,指向函數對象。從而形成一個圓環。此外,函數對象的實例實際上也有一個隱藏的屬性__proto__指向函數的原型對象。

總結一下,一個函數代表着兩個對象,應該是函數對象,一個是函數的原型對象。此外可以通過函數對象調用構造函數生成函數對象的實例,其屬性__proto__指向函數的原型對象。

function test(){
    console.log("test "+this);
    return this;
}

var b = new test();
console.log(b == test);//false
console.log(b instanceof test);//true
console.log(test);//[Function: test]
console.log(test.prototype);//test {}
console.log(test.prototype.constructor);//[Function: test]

默認的原型對象只自己生成了constructor,其餘的屬性實際上都是從Object繼承而來的。

對象屬性搜索機制與原型鏈

事實上,任何一個對象實例都有__proto__屬性,其指向其所在的原型,我們使用instanceof檢查對象類型時,實際上也是通過這個檢查的。進一步講,原型也是一個對象實例,那麼原型對象也有自己的__proto__屬性,從而指向原型的對象類型。事實上,所有的對象最終都指向Object類型。

我們看到,通過__proto__屬性,任何一個對象通過原型都形成了一個通往Object的鏈條,這就是原型鏈。因此使用instanceof檢查對象類型時,所有對象都是Object對象,只要是在原型鏈上的類型,instanceof都會返回true。

那麼當我們訪問一個實例的屬性時,會發生什麼呢?舉個例子A的原型是B,B的原型是Object,現在用A生成一個實例a,代碼如下:

在這裏插入圖片描述

這裏我直接給__proto__屬性賦值,但實際不要這樣做,邏輯上這個屬性是不應該被修改的,如果用在實際中,鬼知道會出現什麼問題。但是從這個代碼中我們就看到了屬性搜索的過程:首先搜索當前對象的所有屬性,如果找到了(例如A.name)那就返回,如果沒有找到,就在它的原型對象中搜索(例如A.age),如果找到了就返回,如果還是沒有找到,繼續沿着原型鏈搜索。如果始終沒有找到就返回undefined。

繼承

有了原型鏈的機制,再談繼承的時候,我們是不是只需要給將父類對象加給子類對象的原型就可以了,這樣一方面子類可以訪問父類的屬性和方法,另一方面父類自己也有原型,這樣就形成了繼承鏈。

但是有一個問題,如果是這樣的話,所有子類都會共享父類的屬性,而沒有自己的屬性。當然這也好辦,子類重寫這些屬性就可以了,這樣一來,子類的屬性就可以屏蔽掉父類的屬性。但如何優雅地實現這一切呢?

組合繼承

//組合繼承
function SuperType(name){
    this.name = name;
    this.colors = ["red","blue","green"];
}

SuperType.prototype.sayName = function(){
    console.log(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(){
    console.log(this.age);
};

var instance1 = new SubType("jack",22);
instance1.colors.push("black");
console.log(instance1.colors);
instance1.sayName();
instance1.sayAge();

var instance2 = new SubType("greg",12);
console.log(instance2.colors);
instance2.sayName();
instance2.sayAge();

console.log(instance1 instanceof SubType);//true
console.log(instance1 instanceof SuperType);//true

// [Running] node "c:\Users\Administrator\Desktop\JavaScript\繼承.js"
// [ 'red', 'blue', 'green', 'black' ]
// jack
// 22
// [ 'red', 'blue', 'green' ]
// greg
// 12

可以看到,子類的構造函數通過call(用apply也可以),調用父類的構造函數,相當於是使用父類對象作爲函數的用法,將子類對象作爲環境變量輸入到父類對象的構造函數當中,從而借用父類構造函數爲子類對象做了一份屬性的拷貝。之後將子類的原型賦值爲父類的實例,從而讓子類擁有了父類的所有方法。由於子類中用父類構造函數重寫了屬性,因此父類中對應的屬性會被屏蔽掉。

如果直接使用父類實例作爲原型,那麼使用instanceof只能檢測到子類對象類型爲父類,所以要將SubType.prototype.constructor設置爲SubType,同時因爲子類原型爲父類實例,父類實例的原型爲父類對象,因此子類也屬於父類。

注意:該方法不可在子類的構造函數內爲子類的prototype重新複製,這樣會導致父類無效。

寄生式繼承

寄生式繼承式根據已有的一個對象創建一個新的對象類型,並增強這個對象的屬性或方法。

/**
 * 該方法實際上是創建了一個空的函數對象,
 * 並把這個函數對象的原型設置爲已有的對象,
 * 從而擁有該對象的特性
 */
function object(o){
    function F(){}
    F.prototype = o;
    return new F();
}

var person = {
    name:"Jack",
    friends:["job","van"]
}
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("rob");

var anotherPerson2 = object(person);
anotherPerson2.name = "linda";
anotherPerson2.friends.push("barbie");

console.log(anotherPerson.name);
console.log(anotherPerson2.friends);
console.log(person.friends);

// [Running] node "c:\Users\Administrator\Desktop\JavaScript\tempCodeRunnerFile.js"
// Greg
// [ 'job', 'van', 'rob', 'barbie' ]
// [ 'job', 'van', 'rob', 'barbie' ]

/**
 * 這個是通過上述方法創建已有對象的副本,
 * 然後添加屬性增強的一個示例
 */
function createAnother(original) {
    var clone = object(original);
    clone.sayHi = function () {
        console.log("hi");
    };
    return clone;
}

一般來說這種方式用處不大,但借用這種方式,我們可以對組合式繼承進一步升級,得到寄生組合式繼承的方式。

寄生組合式繼承

組合模式有一個侷限性,那就是要調用兩次父類構造函數,並且專門爲子類創建了一個父類實例。但實際上,所有需要共享的函數(或屬性)實際上都會放在父類的原型之中,而我們要繼承的父類的屬性都不需要他們共享。通過組合式繼承,我們多創建了一份父類的實例,相當於就多創建了一份父類屬性的副本,但實際上並不會用到。

寄生組合式繼承就解決了這一問題,下面看代碼:

//寄生組合式繼承
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function inheritPrototype(subType, superType) {
    //創建空函數對象,其原型爲父類原型
    //(注意這裏並沒有調用父類構造函數,而是將父類原型直接賦值)
    var prototype = object(superType.prototype);
    //增強對象
    prototype.constructor = subType;
    //指定對象:將子類原型設置爲該對象,相當於之拷貝父類原型
    subType.prototype = prototype;
}

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

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

function SubType(name, age) {
    //借用構造函數(唯一的一次調用父類構造函數)
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SubType, SuperType);

//添加子類方法
SubType.prototype.sayAge = function () {
    console.log(this.age);
};

這裏object()函數通過直接將父類原型賦值過來,從而避免了創建無用的父類實例,而子類調用SuperType.call(this, name);複製了所有的父類屬性。所以十分巧妙。

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