本文中心:
這篇文章比較難懂,所以讀起來比較晦澀。所以,我簡單列一下提綱:
在第一部分,從函數原型開始談起,目的是想搞明白,這個屬性是什么,為什么存在,在創建對象的的時候起到了什么作用!
在第二部分,閱讀的時候,請分清楚__proto__和內置對象的區別;搞清楚這點。然后,我們再一點點分析__proto__屬性。
第三部分,本來不在我寫作的范圍,但是看到網上的很多文章在繼承的時候,使用的方法五花八門。所以來談一下,Object.create()這個方法的好處。
1.函數原型
1.1.函數特有的prototype屬性
標題中所謂的特有,指的是只有函數才具有prototype屬性,ECMAScript標准規定每個函數都擁有一個屬於自己的原型(prototype)。
那么這個函數的原型到底是什么,它又有什么用呢?
- 函數的原型是什么?
用代碼證明:函數原型是一個對象。console.log(typeof Object.prototype); //"object", 這里用到了Object()函數。 console.log(Object.prototype instanceof Object) //true
從上面的輸出結果中,我們得出函數的原型是一個對象。那么,這個對象本身有什么屬性呢?
我們知道,任何一個對象都具有最基本的方法,比如 toString().valueof()...既然函數原型是對象類型,那么它肯定也具有這些基本的方法...
所以這些方法是從哪里來的呢?要想搞清楚這些,那么我們就必須要從Object()的原型談起!
上面這幅圖片,幫我們認清楚了Object()函數的原型,這個函數原型本身不具有任何屬性,但是其具有一些很基本的方法,這些方法有什么用,這里暫且不論。但是到目前為此,請記住一點:函數原型是一個對象。因為只有知道了最基本的這一點,我們下面的討論才具有意義。 - 函數原型有什么用?
只是知道函數原型是一個對象,才只是開始,因為我們想知道的是:函數的原型對象什么用,為什么要要設計這么個東東!!
看下面的一段代碼,我們跟着代碼來分析:
var object = new Object(); //new 一個對象 console.log(object.toString()); //輸出這個對象,firefox控制台下輸出結果 [obejct object]
在object這個對象中,其具有一個__proto__屬性,這個屬性是哪里來的? ......等等......有沒有覺得__proto__的值和Object.prototype的值時驚人的相似呢。難道這是巧合嗎, 還是說他們本來就是同一個對象呢!!!我們來測試一下:
var object = new Object(); console.log(object.__proto__==Object.prototype); //true.
事實再一次證明,世上沒那么多的巧合!!object.__ptoto__和Object.prototype真的指向的是同一個對象。
現在我們解決了一個問題,就是object.toSring()這個函數,真正的來源是Object.prototype。那么object對象為什么能訪問Object.prototype中的方法呢...要回答這個問題,需要弄清楚兩件事情:
第一,當 new Object()到底發生了什么?
第二:__proto__這個屬性起到了什么作用?
要想弄明白上面的兩個問題,我們依然需要分析程序
var object=new Object();
(1).開辟了一些內存空間給object;
(2).將this指針指向object(暫且不論這點,this指針我們以后也會開單題來說).Ok.現在知道了new有什么用了;
(3).將object添加一個內置屬性屬性,__proto__的值和內置屬性的值總是相等的;
知道了當new Object()的時候,解釋器幫我們給對象添加了一個內置屬性,接下來解決第二個問題,內置屬性[[__proto__]]有什么用?
看下面的代碼var object = new Object(); var proto1 = {}; proto1.name = '張三' object.__proto__ = proto1; console.log(object.name); //張三 var proto2 = {}; proto2.name = '李四' object.__proto__ = proto2; console.log(object.name); //李四
總結以上,我們可以得出結論:
每個函數都擁有一個屬於自己的原型,這個原型的實質是一個對象,當該函數被當做構造函數使用(即new調用)的時候,所生成的實例會有一個內置的屬性,當訪問這個對象的時候,如果在實例中沒有找到對應屬性,則會根據內置屬性,查找內置屬性所指向的對象,一直到最上層若找不到則返回undefined.(嚴格模式的時候會報錯).
1.2.函數原型prototype的constructor屬性
在創建一個新的函數的時候,這個函數的原型中會有一個constructor屬性,那么這個屬性是否有存在的意義呢?
看下面的一段代碼:
var Person=function(){}; console.log(Person.prototype.constructor); //function constructor是一個函數 console.log(Person.prototype.constructor===Person);//true Person.prototype.constructor===Person
上述代碼,證明了constructor這個屬性是真實存在的,且這個屬性的值初始化為構造函數本身。那么這個屬性有什么很重要的意義嗎? 再看下面的碼:
var Person = function () { }; var xiaoming = new Person(); console.log(xiaoming instanceof Person); //true Person.prototype.constructor = null; console.log(xiaoming instanceof Person); //true
由上面例子可以得出,constructor屬性只是標識原型是歸屬於哪個函數的,這個屬性雖然是解釋器給我們默認的,但是相對來說沒有那么重要,甚至說起來可以是一點用處都沒有。對於一個函數,在剛創建的時候總是這個樣子的。
1.3prototype的宿命---用於繼承
有些事情在你出生的那一刻就已經注定要發生。prototype在出生之初就已經注定其宿命。下面讓我們來談談這所謂的宿命吧!!
根據1.1部分,我們知道函數的原型,在函數實例化的時候會被賦值給實例的內置屬性的。假設有兩個類A和B,代碼如下:
//A函數如下 var A = function (a) { this.a = a; } A.prototype.getA = function () { return this.a; } // B函數如下 var B = function (a, b) { A.call(this, a); //借用A類構造函數,很重要的一點!!! this.b = b; } B.prototype.getB = function () { return this.b; }
A和B分別是兩個類的構造函數,他們此時在內存中的結構如下圖所示:
現在如果我們想讓B類成為A的子類,該如何做呢? 首先,我們應該認識到一點,如果B是A的子類,那么B就應該能訪問A中的屬性和方法。父類A中有屬性a和方法getA(),那么子類B中也應該有屬性a且能訪問方法getA();如果我們能實現如下圖所示的結構是否就能做到B類繼承A類呢?
與上圖相比,僅僅修改了B.prototype中的【【__proto__】】.然后一切的一切都自然而然的發生了。總之,子類B為了繼承A做了兩樣活: 子類B類通過A.call();這一步借用A的構造函數擁有的A類的變量,又通過修改原型中的【【__proto__】】才做到能訪問A類中的函數..想到這里不得不說一句,好偉大的設計。如果只是為了實現繼承,有N多種方法能實現,但是請注意,如果考慮內存中的分配情況以及效率和程序的健壯性,那么就只有一個函數能夠完美的做到圖中所示的那樣。這個函數就是Object.create()函數,這個函數的宿命就是為了實現繼承。
為什么這么說呢,請看第二部分慢慢解析!!
2.__proto__屬性和內置屬性的區別
2.1.你真的了解__proto__這個屬性嗎?
如果你認為自己很了解這個屬性,那么請思考以下幾個問題?
1.這個屬性是什么性質的屬性? 訪問器屬性 or 數據屬性?
2. 這個屬性存在在哪里? 是每個對象都有,還是在整個內存中僅有一份。
3.這個屬性與內置屬性有什么關系?
如果你對上面的上個問題很困惑,或者你認為__proto__就是內置屬性的話,那么我們還是花一點時間正正三觀吧。
2.1.1.證明1:__proto__是訪問器屬性
看下面一段代碼:
var descriptor=Object.getOwnPropertyDescriptor(Object.prototype,"__proto__"); console.log(descriptor); //輸出結果:
configurable | true |
enumerable | false |
get | function() { [native code] } |
set | function() { [native code] } |
看到上面的輸出結果,你是否已經接受了__proto__就是一個訪問器屬性呢....如果你還不相信..那么接着看,這只是踐踏你世界觀的開始!!!
2.1.2.證明2:__proto__屬性在內存中僅存一份
從證明1的輸出結果中,我們知道configurable=true;這也就告訴我們這個對象是可以被刪除的...下面看一段代碼:
var object={}; var result=delete object.__proto__; console.log(result); //true console.log(typeof object.__proto__) //object.
請回答? 為什么顯示刪除成功了, typeof object.__proto__還是輸出 object呢?
ok!! 要理解透徹這些,我們插入一些delete運算符的知識.
ECMAScript規定:delete 運算符刪除對以前定義的對象屬性或方法的引用,而且delete 運算符不能刪除開發者未定義的屬性和方法。
那么什么情況下,delete會起作用呢?
delelte運算如果想正常操作必須滿足三個條件: 1,該屬性是我們寫的,即該屬性存在。2.刪除的是一個對象的屬性或方法。3.該屬性在配置時是可以刪除的,即(configurable=true)的情況下可以刪除。
那么,上面的例題中,返回值為true.,它符合上面的三個條件嗎?
對於1,該屬性我們是可以訪問的,所以,證明該屬性存在。
對於2,__proto__是某個對象的屬性。
對於3:因為 configurable=true,所以也是符合的。
ok!上面的三點都符合,在返回值等於true的情況下,刪除還是失敗了呢! 因為還有下面一種情況,就是在對象上刪除一個根本不存在的於自身的屬性也會返回true!
var object = { };
Object.prototype.x={}; var result = delete object.x; console.log(result); //true.console.log(object.x);//object
看到沒有,這兩個例子在輸出結果上很相似呢?因為 __proto__屬性存在於該對象上的原型上面,所以,該對象可以訪問。但是不能刪除該屬性。如果想刪除該屬性,那么請在Object.prototype上刪除。這個保證能刪除成功。
為了證實這一點,我們再看一下
var object={}; console.log(typeof object.__proto__); //object delete Object.prototype.__proto__; console.log(typeof object.__proto__); //undefined 刪除成功。
我們可以發現,在Object.prototype刪除__proto__屬性后。object上也無法訪問了。這是因為,所以對象都有一個共同的原型Object.prototype.在這個上面刪除__proto__,那么所有的對象也都不具有這個__proto__屬性了。
這也就證明了,內存中僅存一份__proto__屬性,且該屬性定義在Object.prototype對象上。
2.1.3.這個屬性與內置屬性有什么關系
從某種程度上來說,__proto__如果存在,那么它總是等於該對象的內置屬性。而且在上一篇文章中我們也點出了一點,改變__proto__的指向也能改變內置屬性的指向。所以,如果你固執的把__proto__認為就是內置對象,那也無可厚非。
但是請記住兩點:
1. 內置對象不可見,但是內置對象總是存在的。
2.__proto__如果存在,那么它的值就是內置對象,但是這個__proto__並不總是存在的。
如果你一定認為__proto__就是內置對象,也可以,但是請保證兩點:不要在程序的任何地方用__proto__屬性。或者,如果你一定要用__proto__這個屬性的話,請保證永遠不要修改Object.prototype中的__proto__!!
如果你不能保證這兩點,請遠離__proto__.因為,一旦有人不遵守約定的話,這個bug的危害代價太大。比如,下面這樣...
var A = function () { } A.prototype.say = function () { return 'hello'; } var B = function () { } //子類繼承父類 function extend(subClass, superClass) { var object = { }; object.__proto__ = superClass.prototype; subClass.prototype = object; subClass.prototype.constructor = subClass; } extend(B, A); //B繼承A
var b = new B(); b.say();
上面是一段,毫無問題的代碼...但是如果有一個小白用戶,在某一天執行了下面一句代碼,
var A = function () { } A.prototype.say = function () { return 'hello'; } var B = function () { } function extend(subClass, superClass) { var object = { }; object.__proto__ = superClass.prototype; subClass.prototype = object; subClass.prototype.constructor = subClass; } delete Object.prototype.__proto__; //或則其他的等等 extend(B, A); var b = new B(); b.say(); //TypeError: b.say is not a function 報錯...如果是這種錯誤,調試起來肯定會讓你欲哭無淚的。所以,如果你想寫出好的程序,請遠離__proto__.
2.2.Object.create()應運而生
時無英雄,使豎子成名! JavaScript的今天的盛行,可以說就是這句話的寫照。Object.create()也是這樣,在繼承時並不是我們非用它不可,只是在排除了使用__proto__之后,除了使用這個函數之外,我們沒有其他更好的選擇。
這個函數在W3C中這個函數是怎么定義的呢?
Object.create 函數 (JavaScript)
這是這個函數在W3C中的定義,我來舉個例子來說明這個函數怎么用吧!!!
var A = function (name) { this.name = name; }; A.prototype.getName = function () { return this.name } var returnObject = Object.create(A.prototype, { name: { value: 'zhangsan', configurable: true, enumerable:false, writable:true } });
上述代碼運行完畢之后,returnObject在內存中的結構如圖所示:
看看上面這張圖,在類比1.3中的最后一張圖,如下:
發現是不是,驚人的相似...所以,知道Objece.create()的強大了吧!! 我們分析過,下面這張圖是實現繼承的完美狀態,而Object.create()就是為了做到這些,專業為繼承而設計出來的函數。
下面是一段用Object.create()函數實現子類繼承父類的代碼;
//子類繼承父類,這段代碼在執行delete Object.prototype.__proto__;這段代碼之后仍然可以正常運行。
function extend(subClass, superClass) { var prototype=Object.create(superClass.prototype); subClass.prototype =prototype;
subClass.prototype.constructor = subClass; }
var A = function () {
}
A.prototype.say = function () {
return 'hello';
}
var B = function () {
}
extend(B, A);
var b = new B();
b.say(); //hello
Ok! 我知道,你能用N多種方法實現繼承,但是請記住,在繼承的時候請不要用__proto__這個屬性,因為它沒你想象中俺么可靠。如果你想獲得一個對象的原型,那么這個方法可以做到,Object.getPrototypeOf。與之對應的是Object.setPrototypeOf方法。
也許你也會說,Object.setPrototypeOf方法可以在遠離__proto__的情況下實現繼承啊啊...如果你在看到它的源代碼你還會這么說嗎?
Object.setPrototypeOf = Object.setPrototypeOf || function(obj, proto) { obj.__proto_ _ = proto; //也是用到了__proto__. return obj; }
總結:
整篇文章,從prototype談起,分析了函數的prototype的類型與作用(這個大家都在談)。
在第二部分,我們分析了__proto__,得到的結果,內置屬性和__proto__根本不是一回事。__proto__這個屬性不可靠..撇開,這個屬性是非標准屬性不說,這個屬性隱藏的bug就能致人於死地,所以,在寫程序時,請謹記一點,珍愛生命,遠離__proto__.
在最后,我們淺談了一下,用Object.create()實現繼承的好處。這個部分,很難講的清楚,需要慢慢去體會。
在下一篇中,我們會分析,為什么會說JS中一切皆是對象!。。。