JavaScript系列----面向對象的JavaScript(2)


本文中心:

   這篇文章比較難懂,所以讀起來比較晦澀。所以,我簡單列一下提綱:

  在第一部分,從函數原型開始談起,目的是想搞明白,這個屬性是什么,為什么存在,在創建對象的的時候起到了什么作用!

  在第二部分,閱讀的時候,請分清楚__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對象沒有toString()函數,這里在輸出的時候為什么不報錯呢? ok! 看下面一副圖片:
            
    在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); //李四 
    在上面的例子中,object一直沒有變,但是其屬性__proto__是指向的對象變了。根據上例,我們可以得出結論,對象是可以訪問到屬性__proto__指向的對象所擁有的變量的,而且使用的時候就像是其自己的屬性一樣。

    總結以上,我們可以得出結論:
        每個函數都擁有一個屬於自己的原型,這個原型的實質是一個對象,當該函數被當做構造函數使用(即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)

創建一個具有指定原型且可選擇性地包含指定屬性的對象。

Object.create(prototype, descriptors)
prototype

必需。 要用作原型的對象。 可以為 null。

descriptors

可選。 包含一個或多個屬性描述符的 JavaScript 對象。

“數據屬性”是可獲取且可設置值的屬性。 數據屬性描述符包含 value 特性,以及 writable、enumerable 和 configurable 特性。 如果未指定最后三個特性,則它們默認為false。 只要檢索或設置該值,“訪問器屬性”就會調用用戶提供的函數。 訪問器屬性描述符包含 set 特性和/或 get 特性。 有關詳細信息,請參閱 Object.defineProperty 函數 (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中一切皆是對象!。。。

 

  

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM