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

本文中心:ide

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

  在第一部分,從函數原型開始談起,目的是想搞明白,這個屬性是什麼,爲何存在,在建立對象的的時候起到了什麼做用!測試

  在第二部分,閱讀的時候,請分清楚__proto__和內置對象的區別;搞清楚這點。而後,咱們再一點點分析__proto__屬性。ui

  第三部分,原本不在我寫做的範圍,可是看到網上的不少文章在繼承的時候,使用的方法五花八門。因此來談一下,Object.create()這個方法的好處。this

1.函數原型

 1.1.函數特有的prototype屬性

      標題中所謂的特有,指的是只有函數才具備prototype屬性,ECMAScript標準規定每一個函數都擁有一個屬於本身的原型(prototype)。spa

  那麼這個函數的原型究竟是什麼,它又有什麼用呢?firefox

  • 函數的原型是什麼?

     
    用代碼證實函數原型是一個對象
    console.log(typeof Object.prototype); //"object",   這裏用到了Object()函數。
    console.log(Object.prototype instanceof Object) //true

    從上面的輸出結果中,咱們得出函數的原型是一個對象。那麼,這個對象自己有什麼屬性呢?
    咱們知道,任何一個對象都具備最基本的方法,好比 toString().valueof()...既然函數原型是對象類型,那麼它確定也具備這些基本的方法...
    因此這些方法是從哪裏來的呢?要想搞清楚這些,那麼咱們就必需要從Object()的原型談起!
           
        上面這幅圖片,幫咱們認清楚了Object()函數的原型,這個函數原型自己不具備任何屬性,可是其具備一些很基本的方法,這些方法有什麼用,這裏暫且不論。可是到目前爲此,請記住一點:函數原型是一個對象。由於只有知道了最基本的這一點,咱們下面的討論才具備意義。
    prototype

  • 函數原型有什麼用?
      只是知道函數原型是一個對象,才只是開始,由於咱們想知道的是:函數的原型對象什麼用,爲何要要設計這麼個東東!!
    看下面的一段代碼,咱們跟着代碼來分析:
    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屬性,那麼這個屬性是否有存在的意義呢?3d

看下面的一段代碼:

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中一切皆是對象!。。。

相關文章
相關標籤/搜索