原型繼承補充(prototype和__proto__詳解)

weixin_34239169發表於2017-05-27

在上篇文章中,由於篇幅的原因只是針對建構函式的構造過程和原型鏈的存取進行深入的講解,有點偏原理性的講解,並沒有對___proto___prototypeconstructor這些屬性之間的互相關係以及實際上的應用分析清楚。所以本文的目的就是為了加深對原型繼承的理解,並能夠將其應用在實際上。

prototype

//建立一個建構函式。
function Fruit(){};
//輸出其原型物件:
console.log(Fruit.prototype);複製程式碼

原型繼承補充(prototype和__proto__詳解)
Fruit.prototype.png

//如果手動設定其prototype屬性的話,那麼將改變其原型物件
Fruit.prototype = {
    getName : function(){
        return this.name;
    },
    name : 'fruit',
};
//再次輸出其原型物件,這時候就發生了點奇怪的事情了
console.log(Fruit.prototype);複製程式碼

原型繼承補充(prototype和__proto__詳解)
Fruit.prototype1.png

對比兩張圖片,有沒有發現異常,心細的人可能發現了Fruit.prototype少了一個constructor屬性,那麼Fruit.prototype.constructor屬性到底跑哪去了,我們在控制檯輸出一下看看。

console.log(Fruit.prototype.constructor)
//function Object() { [native code] }
//可能有人會感到奇怪,在Fruit.prototype中明明並沒有該屬性,那麼這該死的constructor屬性從哪裡來的。
//我們回到Fruit.prototype屬性,點開__proto__屬性,那麼一切就明朗了。複製程式碼

原型繼承補充(prototype和__proto__詳解)
Fruit.prototype1.__proto__.png

Fruit.prototype中,只有其的__proto__擁有constructor屬性,所以是不是可以認為其Fruit.prototype.constructor===Fruit.prototype.__proto__.constructor?事實上,我們可以認為二者指向同一個建構函式。由於重寫了Fruit的原型物件,JavaScript引擎不能在顯式原型中找到constructor屬性,那麼它將通過隱式原型鏈查詢,找到了Fruit.prototype.__proto__的constructor屬性。如果重寫原型就會導致constructor屬性的更改,那麼在實際開發的時候就會發生指向不明的錯誤,如下所示:

function Fruit(){}
function Animal(){}
Animal.prototype = new Fruit();
var apple = new Fruit();
var cat = new Animal();
alert(apple.constructor===cat.constructor);//true
//apple和cat明明屬於兩個不同構造器產生的例項,但是它們的constructor屬性指向同一構造器產生的例項複製程式碼

所以在修改建構函式的原型時候,應該修正該原型物件的constructor屬性,通常修正方法有兩種:

//第一種方法:當其原型修改時,手動更改其原型的```constructor```屬性的指向。
Animal.prototype.constructor = Animal;
//第二種方法:保持原型的構造器屬性,在子類構造器函式內初始化例項的構造器屬性,
function Animal(){
    this.constructor = arguments.callee;
    //或者可以:this.constructor = Animal;
}複製程式碼

在網上對constructor屬性的作用有著許多不同的看法,有的人認為其是為了將例項的構造器的原型物件更好的暴露出來,但是我個人認為constructor屬性在整個原型繼承中其實是沒有起到什麼作用的,甚至在JS語言中也是如此,因為其可讀寫,所以其未必指向物件的建構函式,像上面的保持原型構造屬性不變,只是從程式設計的習慣出發,讓物件的constructor屬性指向其建構函式。

說完了建構函式的prototype屬性,由於我在上文就已經介紹過了普通的函式與建構函式並沒有什麼本質的區別,所以現在我們開始將目光放在一些特殊的函式上面。

Function是JavaScript一個特殊的建構函式,在JS中,每一個函式都是其物件(Object也是)。在控制檯輸出下Function.prototype得到這樣一個函式function () { [native code] }。再用

console.log(Function.prototype);
//function () { [native code] }
//用typeof判斷下其型別
console.log(typeof Function.prototype)//function
//既然其是function型別的,那麼因為所有的函式都有prototype對
//象,所以其肯定就有prototype屬性了,那麼我們現在可以輸出看看了,但是神奇的事情發生了。
console.log(Function.prototype.prototype)//undefined
//其居然輸出了undefined,這發生了什麼事情??複製程式碼

翻閱了許多資料,終於讓我找到了其原因所在。而這與JavaScript的底層有關了。在上篇文章,我們就說到了Object.prototype處於原型鏈的頂端,而JavaScript在Object.prototype的基礎上又產生了一個Function.prototype物件,這個物件又被稱為[Function:Empty](空函式,其是一個不同於一般函式的函式物件)。隨後又以該物件為基礎打造了兩個建構函式,一個即為Function,另一個為Object。意不意外,驚不驚喜!但是看到下面,你又會剛到更加意外的。所以,在下面的程式碼如此顯示,你就不會感到意外了。

console.log(Object.__proto__ === Function.prototype);//true
//Object的__proto__屬性指向Function.prototype。這又說明Object這個構造器是從Function的原型生產出來的。
console.log(Object.constructor === Function);//true
//Object.constructor屬性指向了它的建構函式Function
//看著上面的程式碼,是不是能夠得出Object是一個Function的例項物件的結論。複製程式碼

沒錯,Object這個建構函式是Function的一個例項(因為Object是繼承自Function.prototype,甚至可以這樣說,所有的建構函式都是 Function的一個例項。

__proto__

談完了prototype屬性,現在我們開始來看看__proto__屬性,在上篇文章中,我們就已經提到了__proto__指向的是當前物件的原型物件。由於在JS內部,__proto__屬性是為了保持子類與父類的一致性,所以在物件初始化的時候,在其內部生成該屬性,並拒絕使用者去修改該屬性。儘管目前我們可以手動去修改該屬性,但是為了保持這種一致性,儘量不要去修改該屬性。廢話不多說,我們來看看一些示例:

//一個普通的函式
function Fruit(){};
console.log(Fruit.__proto__);//function(){ [native code] }
//貌似有點眼熟,像是上面的空函式,動手試試
console.log(Fruit.__proto__===Function.prototype)//true
//恩,有點大驚小怪了,物件的__proto__就是指向構造該物件的建構函式的原型物件。
//如果二者不等的話,那就出事了。
//現在來看看一個建構函式構造出來的物件
var apple = new Fruit();
console.log(apple.__proto__);
//其指向了Fruit.prototype,但是如果Fruit.prototype該變數,那會怎麼樣呢?
Fruit.prototype = {};
console.log(apple.__proto__);
//貌似跟上面並沒有多大的變化,但是別急,我們接下來看。
var banana = new Fruit();
console.log(banana.__proto__);
//{};這就對了,物件的__proto__就是指向原型物件的,當建構函式的原型物件改變的時候,其也將改變。
//至於為什麼apple和banana的__proto__屬性會變化,這就涉及到記憶體分配的問題了,在這裡就不再展開。複製程式碼

由於每個物件都將擁有一個__proto__屬性,那麼apple.__proto__必然擁有__proto__屬性,那就讓我們一起探究下吧。

function Animal(){};
var dog = new Animal();
console.log(dog.__proto__.__proto__)
//Object {__defineGetter__: function, __defineSetter__: function, hasOwnProperty: function, __lookupGetter__: function, __lookupSetter__: function…}
//是不是很眼熟,這跟上面的Object.prototypey一模一樣,輸出看看
console.log(dog.__proto__.__proto__==Object.prototype) //true複製程式碼

其實仔細分析下就應該知道這樣的指向,dog.__proto__指向Animal.prototype,而Animal.prototype其實是一個物件例項,由Object所構造出來的,自然Animal.prototype.__proto__指向Object.prototype。看完了物件的__proto__屬性,現在來看下函式的相關屬性。

console.log(Animal.__proto__===Function.prototype)//true;
console.log(Animal.__proto__.__proto__===Object.prototype)//true;
console.log(Animal.__proto__.__proto__.__proto__)//null複製程式碼

可能有人會對Animal.__proto__.__proto__.__proto__===null產生疑惑,有人也是因為這樣而認為在整個原型鏈的頂端就是null,其實不然,因為null壓根就沒有任何屬性,自然物件和函式就不能從中繼承到什麼東西了。
其實在JavaScript內部,當例項化一個物件的時候,例項物件的__proto__指向了建構函式的prototype屬性,以此來繼承建構函式prototype上所有屬性和方法。

總結:其實如果能夠縷清__proto__prototype二者的關係,那麼關於原型繼承就很簡單了。每個物件都擁有了__proto__屬性,所有物件的__proto__屬性串聯起了一條原型鏈,連線了擁有繼承關係的物件,這條原型鏈的終點指向了Object.prototype

相關文章