javascript面向對象編程——構造函數的繼承

在編程中我們有時候需要面向不同的對象,在這些不同的對象中我們有時候需要將他們彼此關聯,但是我們怎麼才能做到彼此關聯沒呢!?現在我們就來看看JavaScript面向對象編程中的——構造函數的繼承。
比如:
var Person = function(){}
Person.prototype.sex = "man";

function myfile(name,work){
this.name = name;
this.work = work;
}
這兩個構造函數是有一定關係的,但是如果想讓myfile()這個函數繼承上面的Person()這個函數,那麼我們該怎樣做?
大體來說的話,構造函數的繼承其實可分爲五種方法,但是各有利弊吧,那麼這五種方法分別是哪五種呢!?

一、構造函數綁定(這一種是最簡單的)
說明:其實我們只需要使用call或apply這兩種的任意一種方法就能夠輕鬆的實現將父對象的構造函數綁定在了子對象上。
代碼部分如下:
function Person(){
    this.type = "person";
}
function myfile(name,work){
    Person.apply(this,arguments);	//加上這代碼就能夠輕鬆的實現
    this.name = name;
    this.work = work;
}
var myfile1 = new myfile("zhang","IT");
console.info(myfile1.type);		//這時我們在控制檯上會發現能夠打印出"person"
我們看看控制檯給我們的結果是什麼?

我們可以看見,這個方法是能夠輕鬆實現繼承的效果的。

二、prototype原型屬性模式(這種方法可能比較難以理解,但是卻也是很常見的方法)
代碼部分如下:(總結的時候我會截圖跟代碼相結合,希望能講清楚)。
function Person(){}
Person.prototype.type = "human";

function myfile(name,work){
    this.name = name;
    this.work = work;
}
myfile.prototype = new Person();
myfile.prototype.constructor = myfile;
console.info(myfile.prototype);
var myfile1 = new myfile("zhang","IT");
console.info(myfile1.type);
我們來看看上面加粗後的代碼在控制檯上打印出的結果分別是什麼如下圖:
第一個打印出的是圖(一)

圖(一)
第二個打印出的是圖二

圖(二
從跟圖二圖二可以看出這種prototype原型屬性的方法的確是能夠做到構造函數的繼承的。
那麼,難以理解的地方是哪裏呢!我們看看下面的這兩段代碼:
myfile.prototype = new Person(); myfile.prototype.constructor = myfile;
其實上面這兩段代碼就是使用prototype原型模式的核心所在。
首先我們來解剖一下這一段 myfile.prototype = new Person(); 代碼。
爲什麼要將 myfile.prototype new一個實例化的對象呢!?因爲我們知道new出來的實例化對像都是指向誰?是的都是指向 prototype原型屬性 所指向的那個虛擬的對象,而new Person() 指向的哪個 prototype原型屬性 所指向的虛擬對象呢!?答案當然是名爲Person的構造函數,現在我們應該明白了,原來 myfile.prototype = new Person(); 這一段代碼是想將名爲 Person的構造函數裏prototype原型屬性所指向的虛擬對象賦值給 myfile.prototype,這樣是不是就很輕鬆的將兩個構造函數給關聯起來了呢!當然關聯起來了,但是還有一個小問題,如果我們沒有 myfile.prototype.constructor = myfile; 這一行代碼,直接使用代碼 console.info(myfile.prototype); 打印看看結果是什麼樣的?
如下圖(三)。

圖(三)
有人會說,這也還是繼承了啊!沒出現什麼問題啊!現在的構造函數myfile()依然繼承的是構造函數myfile()的屬性以及其值啊!但是你有沒有發現他是繼承了,但是繼承的太多了,就連構造函數都已經重寫了,也就是constructor構造函數屬性的屬性及其屬性值,都不是我們想要的,我們想要的是下面這一段代碼的內容:
function myfile(name,work){
    this.name = name;
    this.work = work;
}
換句話說,我們其實是想繼承構造函數 Person() 中用 prototype原型屬性 來設置的屬性 Person.prototype.type = "human"; 但是現在就連構造函數都已經繼承了。爲了解決這一問題我們就重寫了,這個 prototype原型屬性 所指向的對象中的 constructor構造函數屬性myfile.prototype.constructor = myfile;將對象中的構造函數屬性重寫爲我們現在的構造函數myfile(),這樣就能夠解決這個問題了。
現在我們在來看看圖(四)重寫之後的打印結果:

圖(四)
是不是發現現在prototype原型屬性所指向的對象中保存的就是
Person.prototype.type = "human";
function myfile(name,work){
    this.name = name;
    this.work = work;
}
這兩個對象,一個是我們使用prototype原型屬性所創建的另外一個是我們創建的名爲myfile()的構造函數。
溫馨提示:(其實這第二種方法理解起來的確有些麻煩,但是如果明白我的上兩章講解的內容,其實你會發現,理解起來也沒那麼麻煩,其實面向對象的這幾章內容的關聯性比較大)
上面這兩章我感覺比較重要,如果你有時間並且有興趣的話可以點進去看看,看後我相信你不僅僅能夠理解這第二種方法,對理解下面的幾種方法也會有很大的幫助。

三、直接繼承prototype原型屬性(這種方法是屬於直接繼承,不像上面使用new一個新的實例化對象)。
爲什麼要直接繼承呢,因爲我們在new一個新的實例化對象的時候其實是很佔用空間,導致空間資源有一點點的浪費。但是這種方法有沒有它自身的弊端呢!下面我們將用實例來進行講解。
代碼部分:
function Person(){}
Person.prototype.type = "human";

function myfile(name,work){
    this.name = name;
    this.work = work;
}
myfile.prototype = Person.prototype;
myfile.prototype.constructor = myfile;
var myfile1 = new myfile("zhang","IT");
console.info(myfile.prototype);
console.info(Person.prototype.constructor);
console.info(myfile1.type);
看看圖(一)上面代碼運行的結果:

圖(一)
從上面的圖(一)看的話感覺這個方法三還是挺靠譜的,我們用console.info(myfile1.type); 代碼執行的結果看見的確是繼承了構造函數Person()中的"type"屬性,並且輸出的值也的確是"human",但是不知道你用代碼跟圖進行比對的時候有沒有發現 console.info(Person.prototype.constructor); 代碼執行的結果爲什麼輸出的是myfile的構造函數?按照正常的邏輯是這個代碼執行輸出的結果應該是Person的構造函數啊!這裏就是方法三問題的所在。

爲什麼會存在這個問題呢!?
原因很簡單:
我們可以看見 myfile.prototype = Person.prototype; 的意思其實就是構造函數Person的原型直接賦給構造函數myfile的原型,換言之就是重寫了構造函數myfile的原型。這樣的話的確是繼承了構造函數Person的原型了,但是原型對象中的constructor構造函數屬性也隨着變成了Person的constructor構造函數屬性了,這不是我們想要的結果,我們知道用myfile.prototype.constructor = myfile;這種方法就可以將原型中的構造函數重寫爲myfile自己的構造函數,我們在使用 myfile.prototype = Person.prototype; 方法賦值後構造函數myfile與構造函數Person都將指向同一個虛擬的對象中,所以就會造成上述的問題,我們在修該myfile的構造函數時構造函數Person也會跟着改變。
其實解決上述的這個問題也很簡單,就像方法二一樣我們new一個實例化對象這種問題就會隱忍而解,因爲實例化的對象只是繼承,而非完全的直接去繼承原型,所以父對象指向的依然是它自己的構造函數,這樣就不會存在繼承後父級跟子集都同時指向同一個虛擬的對象,只是用了new實例化的對象來重寫了子集中的對象內容,但是正如上面所說,這種方式是很佔用空間內存的,於是我們就有了下面的第四種方法。

四、合理運用空對象作爲其中介(其實二三四這三種方法與其說是三種方法不如說其實是一個方法,他們只是一個優化改進的過程,而這裏的第四種方法就是爲了解決第三種方法存在的問題)
這裏我們需要一個空的對象作爲中介,將父級的prototype對象賦給這個空對象,由這個空對象來完成與子對象之間的繼承,這樣子對象既能繼承父對象的prototype對象,同時父對象的prototype對象也不會受到子對象prototype對象的影響,因爲我們創建出來的又是一個空的對象所以幾乎不會佔用空間內存。
下面我們就來看看代碼部分:
function Person(){}
Person.prototype.type = "human";
function myfile(name,work){
    this.name = name;
    this.work = work;
}
function F(){}
F.prototype = Person.prototype;
myfile.prototype = new F();
myfile.prototype.constructor = myfile;
console.info(myfile.prototype);
console.info(Person.prototype);
當我們打印 console.info(myfile.prototype); 代碼的時候控制檯給出的結果如下圖:

如圖所見構造函數即繼承了構造函數Person的prototype原型,同時myfile自己的構造函數也依然保留,那麼構造函數Person的原型是不是還依然存在有沒有受到myfile這個構造函數的影響呢!?
我們來運行 console.info(Person.prototype); 這段代碼看看控制檯會給出什麼樣的結果,如下圖:

我們從控制檯輸出的結果可以看出Person的原型依然不變。
總結:這種方法不僅解決了佔用內存的問題,同時也解決了如果直接將父對象賦值給子對象後父子對象同時會指向同一個prototype原型的問題。
方法四擴展:
A:
function Person(){}
Person.prototype.type = "human";
function Myfile(name,work){
    this.name = name;
    this.work = work;
}
function material(Child,Parent){
    var F = function(){};
    F.prototype = Parent.prototype;
    Child.prototype = new F();
    Child.prototype.constructor = Child;
    Child.uber = Parent.prototype;
}
material(Myfile,Person);
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.type);
有沒有看見上面代碼有什麼不同?是的我們用了兩個參數Child與Parent來將代碼進行封裝了,如果對面向對象的封裝不明白的可以看看js面向對象之封裝(構造函數)我的這一章,裏面有詳細說明。
B:
如果你之前看過我的prototype原型屬性的這一章的話,在這章裏有一個細節地方你肯定會產生疑問,在prototype原型屬性的這一章裏我們說過new的實例化對象時沒有prototype原型屬性的,prototype原型屬性是在構造函數創建的時候會自動生成一個原型對象,它與對象實例化沒有任何關係。但是你可以看看這行代碼: myfile.prototype = new F(); 我們運行 console.info(myfile.prototype); 這行代碼在控制檯上輸出不僅不會報錯,而且還給我們返回了我們想要的值,有沒有覺得有些矛盾,這裏明明是將F這個構造函數實例化了啊!?
我們運行下面的這段代碼:
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.prototype);
看看下圖的控制檯會輸出什麼?

是的,輸出的是undefined。
但是如果你仔細對比一下這兩段代碼你會發現問題的所在:
myfile.prototype = new F(); 與 var Myfile1 = new Myfile("zhang","IT");
其實new的實例化對象時真的沒有prototype原型屬性,但是每一個構造出來的實例化對象內部都會自動生成一個[[proto]]這個屬性,正是通過這個屬性來指向prototype這個原型對象中的, 而 myfile.prototype = new F(); 其實就是重寫構造函數myfile的原型,而 var Myfile1 = new Myfile("zhang","IT"); 則纔是真正的將實例化對象保存在一個我們創建的變量中,原型雖然被重寫但是它依然還是原型故里面是存在prototype原型屬性的,而真正的在變量中保存的實例化對象,那就是實力化對象所以並沒有prototype原型屬性,希望大家能夠理解這個地方,不要混淆了。

五、拷貝繼承法(何爲拷貝繼承法,顧名思義就是將父對象的原型拷貝也就是複製到子對象中)
我們看看下面這段代碼:
function Person(){};
Person.prototype.type = "human";
Person.prototype.sex = "nan";
Person.prototype.age = "100";
Person.prototype.phone = "11111";
function Myfile(name,work){
    this.name = name;
    this.work = work;
}
function material(Child,Parent){
    var c = Child.prototype;
    var p = Parent.prototype;
    for(var i in p){
        c[i] = p[i];
    }
    c.uber = p;
}
material(Myfile,Person);
var Myfile1 = new Myfile("zhang","IT");
console.info(Myfile1.type);
console.info(Person.prototype.constructor);
console.info(Myfile.prototype);
其實這個方法也比較好用跟第四種方法差不多,拷貝繼承的核心關鍵在於用 for....in 語句來循環,循環父對象中的所有屬性然後複製給子對象
function material(Child,Parent){
    var c = Child.prototype;
    var p = Parent.prototype;
    for(var i in p){
        c[i] = p[i];
    }
    c.uber = p;
}
這個地方是關鍵。
下面我們就來看看用這種方法在瀏覽器的控制檯中輸出的結果是不是我們想要的。
運行 console.info(Myfile1.type); 結果如下圖:

很顯然是正確的,說明目前是正常的繼承了Person的原型對象。
運行 console.info(Person.prototype.constructor); 我們看看Person的構造函數屬性有沒有被改變掉。
如下圖:

哎!!!我們發現目前的Person原型中的構造函數屬性依舊指向的是Person,並沒有被改變,挺好。
運行 console.info(Myfile.prototype); 我們看看現在構造函數Myfile的原型中都保存着哪些東西。
如下圖:

我們可以很明顯的發現現在的構造函數Myfile的原型中保存的是從Person這裏繼承來的所有對象,以及自己的構造函數屬性中的全部對象。
總結:其實 for....in 循環的是父對象中的所有屬性,然後將循環的屬性都保留在了變量 i 中,這樣就實現了循環複製的效果,而不像方法二方法三中那樣,是將父對象中整個的原型都賦值給了子對子對象,而方法五是隻複製了屬性,但是constructor構造函數屬性卻沒有被複制,換言之Person函數與Myfile函數依然在constructor構造函數屬性中依然還是獨立的個體他們並沒有方法三一樣同時指向了同一個原型對象。
我們來看看 i 中都保存了什麼,你就會恍然大悟,我們運行如下代碼:
function material(Child,Parent){
    var c = Child.prototype;
    var p = Parent.prototype;
    for(var i in p){
        c[i] = p[i];
    }
    c.uber = p;
}
看看控制檯會給我們什麼樣的結果:

這裏就是 變量i 中保存的屬性,我們看見了這裏並沒有原型中constructor構造函數屬性。
友情提示:在我的面向對象講解的章節中,會出現一些我沒有過多講解的小屬性,比如 uber屬性 或是在控制檯中會出現 __proto__屬性,這些東西都是些什麼?有什麼作用?我會在將面向對象剩下的內容講完後,專門用一章的篇幅對這些小屬性進行講解,其實這個不會對理解造成影響,就當是對面向對象的補充吧。

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