文章目錄
本文同時發布在另一獨立部落格 Javascript: 從prototype漫談到繼承(1)
javasscript的prototype原型鏈一直是一個痛點,這篇文章是對自己這段時期學習的一個總結,在這裡不談ECMAScript標準,也不會用UML圖畫出各種關係(結合這兩方面談的文章非常的多,但大部分都相當晦澀,比如湯姆大叔),只力求最淺顯易懂,深入淺出,供以後自己和各位參考。
javascript的function一種對象(object),他們有方法和屬性,方法比如call/apply,而prototype則是function的一個屬性。
一旦你定義了一個函數,它即內建了一個prototype屬性
function t(){};
typeof t.prototype // "object";
你可能已經知道使用函數作為一個建構函式,來生產一系列對象。比如
function Some(name, color){
this.name = name;
this.color = color;
this.method =function(){}
}
var a1 =new Some("Lee","black");//執行個體化一個對象
上面的Some類的屬性和方法也可以放在prototype對象中,比如
function Some(){}
Some.prototype.name ="Lee"//形式一
Some.prototype ={ //形式二 name:"lee", color:"black", method:function(){}
}
var a1 =newSome("Lee","black");//執行個體化一個對象
雖然形式不同,但至少現在使用起來的效果是一致的。當你使用a.Lee或者a.method時,結果是一樣的,現在還看不出分別
Ok,那麼第一點要注意的是,prototype是活著(live)的屬性!
function Some(){}
var a =newSome();
a.method // undefined
Some.prototype.method = function(){ console.log("hello");
}a.method // function () {console.log("Hello")}
上面的代碼想說明的是,在產生執行個體a時,建構函式沒有method方法,所以a也沒有,可以理解;但是之後建構函式在prototype屬性裡又添加上去了,雖然是在a產生之後添加的,但是a仍然照樣擁有,與建構函式添加的時間無關。
第二個問題來了,如果這個對象內部和prototype都定義了相同的欄位怎麼辦,比如
function Some(){
this.color ="yellow";
}
Some.prototype.color ="black";
var a =newSome();
a.color //?
上面的代碼中,我在對象的內部和prototype上分別都定義了color,當我從執行個體中訪問的時候,應該顯示的是哪一個顏色?
要注意的是第二點,javascript引擎首先會檢查a的屬性裡有沒有color,如果沒有的話去它的建構函式的prototype(a.constructor.prototype)裡有沒有該屬性
讓我們再看的遠一點,任何一個對象都應該有自己的建構函式,函數的prototype屬性也是個對象,那它的建構函式是什嗎?
functionSome(){
this.color ="yellow";
}
var a =newSome();
a.constructor.prototype.constructor // function Some() {this.color = "yellow";}a.constructor.prototype.constructor.prototype // Some {}
上面的原型鏈可以無限的追溯下去,通過原型鏈,可以追溯到最終的建構函式Object()
,這也就解釋了,為什麼即使我們沒有在函數上定義toString()
函數,a.toString()
的方法也是存在的,因為它最終調用的追溯到的Object的toString方法。
新的問題是,如何區分自己的property和原型鏈上的屬性,並且你能保證所有的屬性都是可以訪問的嗎?
眾所周知,用for...in
迴圈就可以解決這個問題,關於這個問題,只需要記住三點
- 雖然在迴圈中對象自己的屬性和原型鏈屬性都會被列舉出來,但並非所有屬性都會被列舉,比如一個數組的length和.splice之類的方法就不一定會被列舉出來,可以列舉出來的屬性都是可枚舉的(enumerable)
- 如何區分對象自己的屬性還是原型鏈的屬性?使用
hasOwnProperty()
方法
- 注意
propertyIsEnumerable()
方法,雖然該方法名字是“可枚舉的屬性”,但是原型鏈中所有的屬性都會反悔false,即使是可枚舉的
還有一個對象的屬性叫做__prop__,個人認為用處不大,只推薦在調試的時候使用,具體用法google去吧
關於原型的繼承
如何寫一個好的繼承方法?這是一個逐漸演化過程,先從最簡單的繼承談起
function Parent(){
this.deep ="Hello";
}
function Child(){
this.shallow ="World";
}
Child.prototype =new Parent();
var c =newChild();
console.log(c.deep);
當我們要訪問c的deep屬性時
- 首先去c對象下查看有沒有deep屬性,沒有
- 再去c.construct.prototype對象的屬性裡尋找,Parent的執行個體裡尋找,有
但是上面的代碼有一個問題,當你不斷執行個體化Child時,Parent也不會被執行個體化,都會產生一個deep載入記憶體中,如果這個deep是共用的話,不如把deep放在prototype中
function Parent(){}
Parent.prototype.deep ="Hello";
function Child(){
this.shallow ="World";
}
Child.prototype =new Parent();
var c =newChild();
console.log(c.deep)
當我們要訪問c的deep屬性時
- 首先去c對象下查看有沒有deep屬性,沒有
- 再去c.construct.prototype對象的屬性裡尋找,Parent的執行個體parent裡尋找,沒有
- 再去parent.construct.prototype尋找deep,有
這麼做的弊端之一就是在尋找某個屬性的時候可能會多尋找一輪
讓我們繼續改進,我們發現我們需要的deep只在Parent的prototype上,那麼其實我只需要Parent的prototype而不是Parent的執行個體
function Parent(){}
Parent.prototype.deep ="Hello";
function Child(){
this.shallow ="World";
}
Child.prototype = Parent.prototype;
var c =new Child();
console.log(c.deep);
這樣既避免了Parent的執行個體化,又避免了上一個例子中多一步的尋找。但是有一個副作用,因為是對對象直接的引用,所以當Child.prototype.deep被修改時,Parent.prototype.deep也會被修改。那我們繼續最佳化的目標就很明確了,要阻止這種對父類prototype的直接引用。
於是我們決定使用一個中間變數
function Parent(){}
Parent.prototype.deep ="Hello";// 注意,來了var F =function(){};F.prototype = Parent.prototype;
function Child(){
this.shallow ="World";
}
Child.prototype = new F();
var c = new Child();
console.log(c.deep)
我們用F來作為一個中間變數,來阻止child對deep的修改可能影響parent
當我們要訪問c的deep屬性時
- 首先去c對象下查看有沒有deep屬性,沒有
- 再去c.construct.prototype對象的屬性裡尋找,F的執行個體裡尋找,沒有
- 再去F.construct.prototype尋找deep,有
讓我們來捋一捋為什麼對Child.prototype的修改不會影響Parent.prototype
- 在上一個例子中,我們對Child.prototype的操作就是對Parent.prototype的操作,無論讀還是寫,用的是別人的
- 在這個例子中,Child.prototype不是對Parent的直接引用,而是一個新的Null 物件。在沒有deep而我們需要deep時,被迫去Child.prototype的建構函式上去找,追溯到了Parent.protoype,而當我們需要寫時,操縱的其實是
Child.prototype = {}
這個Null 物件。
於是我們把最後一個程式碼片段抽象為一個方法
function extend(Child,Parent){
var F =function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
// 一旦重設了函數的prototype,需要重新賦值prototype.constructor,
// 忽略這方面的介紹
Child.prototype.constructor = Child;
// 保留對父類的引用,
// 忽略對這方面的介紹
Child.uber =Parent.prototype;
}