走心大白話JavaScript教程(三)不得不說的原型與原型鏈

Terry豆發表於2017-04-27

JS大法好,JS在手,天下我有,信JS,得永生。

這個系列的教程我一開始是寫在github上的,
但是覺得放到掘金來可以讓更多需要的人看到,
就搬到掘金專欄上啦,
如果覺得本教程對你有幫助,請點這裡去github上給我一顆Star~
教程目錄也在github上哈~

本著對技術負責的態度,任何糾正/疑問,儘管提出,我會及時修正/回答。

一定要把每個例子程式碼都拷貝到你的執行環境中邊看結果邊理解,不然學習效果減半,或者沒效果。

下面開始第三篇:

不得不說的原型與原型鏈:prototype與__proto__


1、紙面意思

啥是原型,啥是原型鏈?

原型:一個屬性,屬性名叫prototype,只有建構函式有,比如Foo.prototype;

原型鏈:一個屬性,屬性名叫__proto__,萬物皆有,鏈狀相連,最後歸宗到Object.prototype上,Object.prototype__proto__值為null;


2、嘗試理解一下這兩個點,記住一定配合控制檯去列印。

  • 訪問一個例項的屬性,JS會先在例項內部尋找,找不到的話,沿著原型鏈繼續找下去。

      function Foo(){this.name='我是建構函式Foo'} //一個建構函式Foo
    
      var foo = new Foo(); //通過new Foo(),得到例項化的foo
    
      console.log(foo.bar); //訪問foo的bar屬性,控制檯列印一下,得到undefined
    
      Foo.prototype.bar = '我是bar屬性,在Foo.prototype(Foo的原型)裡';  //給Foo.prototype新增bar屬性
    
      console.log(foo.bar); //再次訪問foo的bar屬性,控制檯列印一下看看。結果驗證了"沿著原型鏈找下去"這句話。
    
      Object.prototype.examAttr = '我是examAttr屬性,在Object.prototype(Object的原型)裡'; //給Object.prototype新增examAttr屬性
    
      console.log(foo.examAttr); //訪問foo的examAttr屬性,控制檯列印一下看看。結果驗證了"一直找到Object.prototype"這句話。複製程式碼

    配合之前寫的紙面意思,回味一下上面的程式碼。

  • 每個建構函式都有一個原型,這個原型的constructor屬性就是這個建構函式。

      function Foo(){this.name='我是建構函式Foo'} //一個建構函式Foo
    
      console.log(Foo.prototype); //列印結果可以看到一個Object物件,即Foo的原型,裡面有一個constructor屬性,屬性值即為Foo函式。
    
      var foo = new Foo(); //例項化
    
      console.log(foo.constructor); //foo中沒有constructor屬性,沿著原型鏈找到Foo的原型(即上面列印的結果),得到Foo原型的constructor屬性值,即Foo函式。複製程式碼

    以上程式碼解釋了為什麼通過檢視例項的constructor屬性可以得到例項的建構函式。重點在於“沿著原型鏈找”。

    3、從老生常談的JS實現new運算子過程來剖析

    //寫一個建構函式,定義其prototype
    function Foo(name) {
      this.name = name;
    }
    Foo.prototype = {
      constructor:Foo, //由於重新定義了prototype,我們們把constructor屬性補上。
      say: function () {
          console.log('My name is ' + this.name);
      }
    };
    //JS實現new的方法generate
    function generate(Fun,arguments) {
      var foo = {}; //新建一個空物件
      Fun.apply(foo, arguments); //利用apply改變this指向,現在執行Fun時,內部this指向foo空物件,那麼給this.name賦值就變成了給foo.name賦值。
      foo.__proto__ = Fun.prototype; //把foo的__proto__屬性指向Fun.prototype。
      return foo;
    }
    //執行generate,模擬new
    var foo = generate(Foo,["Terry"]); //相當於 var foo = new Foo("Terry")
    //驗證例項方法
    foo.say();
    //檢視foo的建構函式
    console.log(foo.constructor);複製程式碼

    上面的程式碼完全實現了new運算子的邏輯,所以說,new運算子就是上面這段程式碼的語法糖而已。

    下面這張圖想必很多人都很熟悉,我擷取了小上半部分,
    不要考慮看不見的導線,只關心f1、Foo、Foo.prototype三者的關係就夠了。
    配合下面這張圖,再回頭看一下JS實現new的過程:

走心大白話JavaScript教程(三)不得不說的原型與原型鏈

理不清沒關係,我們來解析一下:
圖中表示,f1由new Foo而來,而f1的__proto__連線著Foo.prototype,
這說明Foo的例項f1的__proto__(原型鏈)是指向Foo.prototype(原型)的,
你再回頭去看我們用JS實現new所封裝的方法generate,有這麼一句:
foo.__proto__ = Fun.prototype;
那不就是手動把foo(圖中的f1)的原型鏈指向Fun(圖中的Foo)的原型嗎!複製程式碼


4、ES5物件導向中常用的混合模式

在封裝程式碼/外掛的時候,使用物件導向的混合模式來寫,程式碼結構是這樣的:複製程式碼
function MyPlugin(name){
    this.name = name; //每個例項都不一樣的屬性,寫在建構函式裡。
}
MyPlugin.prototype = {
    publicFun:function(){
        console.log('我是公共方法,所有例項共用'); //每個例項都呼叫一樣邏輯的程式碼,封裝成方法寫進建構函式的原型裡
        console.log(this.name+'使用了外掛'); 
    }
}
//使用外掛
var myPlugin = new MyPlugin('小明');
myPlugin.publicFun(); //例項並沒有publicFun方法,但是JS從myPlugun.__proto__中找到了public。複製程式碼

通過找“點”大法(前面教程有說過),可以發現publicFun內部的this指向myPlugin,
也就驗證了為什麼1、理解this指向的小技巧中小結裡的第三點:
“明確區分函式是[建構函式]還是[普通函式],[建構函式]內的this指向例項化後的物件;”

這回知道為什麼外掛可以這麼寫,並且例項化後的外掛可以直接呼叫寫在prototype裡的方法了吧?

小結

  • 關於原型與原型鏈,沒有什麼“一句話來說”,仔細通讀這篇教程吧,尤其是那張圖。

PS:
歡迎轉載,需要註明原址。
教程之間緊密聯絡,不懂的地方,請好好看下全系列教程目錄
有沒有你不懂的那個關鍵字在裡面。
如果幫到你,別忘了給我一顆Star~

相關文章