深入理解JavaScript之原型鏈

  1   原型是什麼?

  原型是什麼?有許多JavaScript初學者都不明白原型。

  說白了,原型就是一個對象中內置的特殊屬性_proto_,它是該對象對於其他對象的引用。

  每個對象在創建時都會被賦予一個非空的私有屬性_proto_指向它的原型對象(任何對象都能做原型對象),該原型對象同樣是對象,既然是對象,那麼它內部也有一個非空的私有屬性_proto_,那麼這個_proto_屬性指向的是什麼呢?這個_proto_指向的是這個原型對象的原型對象。層層向上直到有一個對象的原型爲"null"爲止。"null"沒有原型,作爲原型鏈(由一層又一層相互關聯的原型指向組成)的最後一個環節。

  一般而言在JavaScript中的對象都是位於原型頂端"Object"的實例,因此很多對象纔會具有"hasOwnProperty(...)"  "toString(...)"  "valueOf(...)"等等的方法。

  這麼說有點抽象,我們來試試用例子來表示。

var foo={};

 我們創建了一個對象"foo",這個函數被創建時帶有非空私有屬性_proto_,這個屬性指向的是"foo"對象的原型對象。

  於是"foo.prototype=Object",接下來"Object.prototype"指向的是"null",這就是原型鏈。

原型鏈:

foo.prototype------>Object.prototype----->null

  注意!!![[Prototype]]與_pro_相同,都是一個對象的內置私有屬性,在創建對象時,自動創建,代表的含義是:指向當前對象的原型對象。而prototype是函數特殊的屬性(此處的prototype與上文對象的prototype不一樣)指向的是一個對象,但是使用"new"構造調用函數創建對象時,"對象._pro_===函數.prototype"。

  prototype是函數特有的,_pro_([[prototype]])是對象的內置私有屬性,函數又是可調用的對象,所以它也具有_pro_。

 

1.1  基於原型鏈的繼承

  這種原型鏈看似缺點很多,但是卻非常容易構建面向對象編程中的繼承

  1.1.1繼承屬性

  ①訪問屬性

 讓我們來回顧一下,我們在《深入理解Java之對象》https://blog.csdn.net/qq_41889956/article/details/83716216中提到的[[Get]]查找。

  1. [[Get]]是查找對象屬性觸發的操作
  2. 它首先會在對象中查找是否存在同名屬性,找到返回,找不到進行下一步
  3. 開始查找對象本身的原型鏈,層層向上,直到查找到該屬性。
  4. 若仍是找不到,返回"undefined"
var foo={
      a:2
  };      //原型鏈:foo.prototype---->Object.prototype---->null
  console.log(foo.a);      //2    訪問對象的屬性,開始在對象foo中查找屬性a找到則返回該屬性的值。

    var bar=Object.create(foo);     //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
     //原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
    console.log(bar.a);              //2  在bar上開始查找屬性a,發現查找不到,於是通過原型鏈查找foo,在foo中查找到屬性a,於是輸出
    console.log(bar.b);              //undefined  同樣在查找bar本身之後遍歷bar相關的原型鏈,發現不存在屬性b,輸出undefined

  "foo.a"時,首先在"foo"中查找屬性"a",找到並返回"a"的值。

  我們利用"Object.create(...)"創建了一個新對象"bar",讓它與對象"foo"的原型關聯起來,實則就是"bar._proto_=foo"。這時的原型鏈變爲:bar.prototype---->foo.prototype---->Object.prototype---->null。

"bar.a"在對象"bar"中查找無果,向原型鏈遍歷,直到在"foo"中查找到"a",於是返回"a"。

"bar.b"在對象"bar"中查找無果,向原型鏈遍歷,直到遍歷原型鏈結束了以後,仍沒有查找到"b",於是返回"undefined"。

注意在當前對象查找屬性,只會從此對象自身的原型鏈開始查找,即向上查找,而不是向下查找。

  在上面的代碼中假設添加這麼一條語句"bar.c=3;"  "console.log(foo.c)"  輸出結果是undefined,爲什麼呢?因爲訪問對象"foo"的屬性"c"於是一開始在自身查找該屬性,查找不到便開始遍歷原型鏈,"foo.prototype---->Object.prototype---->null",查找無果輸出undefined。

 

②屏蔽屬性

  在屬性中因爲原型鏈的存在讓我們得以繼承屬性,實現面向對象中的繼承特性,那麼當我們在對象自身中定義了一個與原型鏈上的屬性同名的屬性,會發生什麼事呢?

 var foo={
      a:2
  };      //原型鏈:foo.prototype---->Object.prototype---->null
  console.log(foo.a);      //2

    var bar=Object.create(foo);     //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
     //原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
     console.log(bar.a);     //2
    bar.a=3;
    console.log(bar.a);     //3   發生了屏蔽
    console.log(foo.a);      //2

當我們在對象自身中定義了一個與原型鏈上的屬性同名的屬性,就會發生屏蔽,然而並不是所有情況下都會發生屏蔽

 

  之前講了[[Get]]操作,那麼我們現在來講講[[Put]]操作設置屬性。

  我們向一個對象中添加一個屬性,會發生怎楊的操作呢?

  1. 當對象屬性中存在這個同名屬性,那麼修改它
  2. 當對象屬性不存在此屬性,原型鏈上存在此屬性,根據種種情況會發生屏蔽與不屏蔽
  3. 當對像屬性不存在此屬性,且原型鏈上不存在此屬性,那麼創建它並賦值

下面我們就第二種情況進行分析,“當對象屬性不存在此屬性,原型鏈上存在此屬性”。

①當對象屬性不存在,且原型鏈上存在此屬性,該屬性還是一個普通的“屬性描述符”(這裏涉及到對象內容的知識,不會的話看https://blog.csdn.net/qq_41889956/article/details/83716216)"writable"爲"true"時,那麼發生屏蔽

 var foo={
      a:2    //普通的屬性描述符
  };      //原型鏈:foo.prototype---->Object.prototype---->null
  console.log(foo.a);      //2

    var bar=Object.create(foo);     //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
     //原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
     console.log(bar.a);     //2
    bar.a=3;
    console.log(bar.a);     //3   發生了屏蔽
    console.log(foo.a);      //2

 

②當對象屬性不存在,且原型鏈上存在此屬性,該屬性還是一個普通的“屬性描述符”,但是與①不同"writable"爲"false"時,不發生屏蔽,如果是在嚴格模式下,還會報錯"TypeError",因爲嚴格模式不允許修改一個只讀的屬性。如果不是嚴格模式就會忽略此語句。

 var foo={
  };      //原型鏈:foo.prototype---->Object.prototype---->null
  Object.defineProperty(foo,"a",{
      writable:false,
      value:2
  });
  console.log(foo.a);      //2

    var bar=Object.create(foo);     //使用Object.create創建對象,此方法能將創建的對象與foo對象強行關聯到一起
     //原型鏈:bar.prototype---->foo.prototype---->Object.prototype---->null
     console.log(bar.a);     //2
    bar.a=3;
    console.log(bar.a);     //2   不發生屏蔽

 

 

③當對象中不存在該屬性,且原型鏈上存在此屬性,該屬性還是一個"setter",那就會調用這個setter,此屬性不會添加到對象中,也就是不會發生屏蔽,這個setter也不會被重新定義。

 

總的來說只有第一種情況:對象中不存在該屬性,且原型鏈中存在該屬性,該屬性還是一個普通屬性描述符"writable"爲"true",纔會發生屏蔽。

 

③繼承方法

  在JavaScript中沒有其他語言中定義的方法,與之稱之爲方法不如說是函數,函數在JavaScript中是一個可調用的對象,那麼繼承函數與繼承屬性沒太大的區別,唯一的區別在於,函數中如果存在this的話,this綁定的對象或許會發生改變。

  當繼承的函數被調用時,this指向的時當前被繼承的對象,而不是繼承的函數所在的原型對象。

 var o={
     a:2,
     foo:function () {
         return this.a;
     }
 };      //原型鏈   o.prototype---->Object.prototype
 console.log(o.foo());    //2
 var m=Object.create(o);   //構建原型鏈  m.prototype---->Object.prototype---->null
    m.a=3;
   console.log(m.foo());   //3   this綁定到了m上面

  可以看到this綁定的對象由"o"變成了"m"。

 

2、創建對象以及生成原型鏈

  前面我們已經知道了使用"Object.create(...)"可使創建對象與參數對象相關聯,從而構建原型鏈,難麼除此之外還有哪些方法呢?

2.1 語法結構創建對象生成原型鏈

  創建普通對象,它的原型對象是"Object"。所以它具有的屬性是來自於"Object",其中包括:hasOwnProperty()

var o= {
     a: 2,
 };    //原型鏈:o.prototype---->Object.prototype---->null

    創建數組對象,數組對象的原型都是"Array","Array.prototype"又指向"Object"因此數組對象具有"forEach(...),indexOf(...)"等方法

 var o=["a",1,"2"];
    //原型鏈:o.prototype---->Array.prototype---->Object.prototype---->null

  創建函數,儘管我們都在說函數是“可調用的”對象,但是它的原型對於普通對象有點不一樣,它的原型對象是"Function","Function.prototype"又指向"Object"。因此它具有"call(...)、bind(...)"等方法

function o() {
     console.log("this a function");
 }              //原型鏈:    o.prototype---->Function.prototype---->Object.prototype---->null

 

2.2  構造器創建的對象生成原型鏈

  回顧下我們之前所說的"new",在JavaScript中是不存在構造方法的,所以當我們使用"new"創建一個對象時,是構造調用,對於一個函數的構造調用。

  使用new關鍵字來創建對象的過程,大體分爲以下三步。

  1. 創建一個對象
  2. 這個函數被執行原型鏈
  3. 這個對象被綁定到函數調用的this上
  4. 如果函數中沒有返回這個對象,那麼new表達式中的函數會自動返回這個新對象
function foo() {
     
 }
 foo.prototype={           //相當於foo的原型對象,因爲bar._pro_===foo.prototype,所以它們共享foo.prototype中的屬性和方法
     a:2,
     tostring:function (a) {
       return   this.a=a;
     }
 };
 var bar=new foo();
 console.log(bar.a);
 console.log(bar.tostring(3));
 console.log(bar.__proto__===foo.prototype);   //證明bar的原型對象確實是foo.prototype
 console.log(Object.getPrototypeOf(bar)===foo.prototype);

    從上述結果可以看出,對象._proto_===使用new創建該對象的函數.prototype,於是它們共享一個對象中的屬性和方法。具體的_proto_與prototype與[[Prototype]]區別可參考https://blog.csdn.net/qq_41889956/article/details/84234001

那麼按照如此上述使用new創建對象便可分爲一下步驟

  1. 創建一個對象
  2. 創建對象._proto_=構造調用函數.prototype
  3. 這個對象被綁定到函數調用的this上
  4. 如果函數中沒有返回這個對象,那麼new表達式中的函數會自動返回這個新對象

  

2.3  Object.create(...)創建對象生成原型鏈

  這是強制使創建對象於參數對象相關聯,創建對象的原型對象=參數對象,形成原型鏈

var o={
     a:2
 };
 var m=Object.create(o);    //原型鏈:   m.prototype---->o.prototype---->Object.prototype---->null
   console.log(m.a);   //2 

 

3  性能

  在原型鏈上查找屬性比較耗費時間,有損性能,比如你在查找一個對象的屬性時,當這個屬性不存在此對象中,就會遍歷整條原型鏈(當然從此對象的原型對象開始)。

  如果你只是像看看再這個對象中是否存在此屬性,那麼可以使用"hasOwnProperty(...)"

  "hasOwnPrototype(...)"是JavaScript中僅有的兩個不會遍歷原型鏈屬性的方法之一,另一個是"Object.key(...)"。

 

4、Object.getPrototypeOf(...)

  此方法的作用是,得到傳入對象的原型對象,與對象foo"foo._pro_"相同。

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章