給程式設計師看的Javascript攻略 - Prototype (下)

古鐵發表於2017-09-08


給程式設計師看的Javascript攻略 - Prototype (下)

原文發表在: holmeshe.me , 本文是漢化重製版。

本系列在 Medium上同步連載。

還記得早先用ajax胡亂做專案的時候踩過好多坑,然後對JS留下了“非常詭異”的印象。最近換了一個工作,工作語言就是JS。然後發現這個語言真不得了,前面後面都能幹,基本成了全棧的同義詞。所以剛好就趁這個機會系統學習一下這個語言。因為是給程式設計師看的,這個系列不會講基本的“if-else”,迴圈,或者物件導向。相反,我會關注差異,希望能給您在Pull Request裡走查程式碼式的學習體驗!

類繼承在JavaScript裡面可以用原型鏈實現。任何一個物件都可以用和自己繫結的原型,像親子鑑定一樣在原型鏈回溯到自己的親爹,以及親爺爺等等。原型鏈也天然支援繼承的最終目的,程式碼複用:當訪問一個當前物件裡不存在的成員時,JS引擎會從當前物件開始,在原型鏈上,向上查詢整合(子)樹的成員資訊。

簡單而不容易

光用說的不太好理解,接下來我會一步一步實現這個原型鏈,所以這個閱讀更像一個模擬的真實開發過程,我覺得搞成這樣可能會比較容易記。

打上碼:

var LaoWang = function() {
  this.aproperty = 'a';
  this.amethod = function () {
    return 'b';
  };
};


var XiaoMing = function() {};

XiaoMing.prototype = LaoWang.prototype; //let's make the chain
var subobj = new XiaoMing();

alert(subobj instanceof LaoWang);
alert(subobj.aproperty);
alert(subobj.amethod());複製程式碼


執行結果:

true
undefined

Uncaught TypeError: subobj.amethod is not a function複製程式碼


雖然型別是正確識別了,但是成員訪問還有問題。所以按一般經驗,我猜是父類的建構函式沒調。試一下:

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };


  var XiaoMing = function() {
++  LaoWang();
  };

  XiaoMing.prototype = LaoWang.prototype;
++XiaoMing.prototype.constructor = XiaoMing;
  var subobj = new XiaoMing();

  alert(subobj instanceof LaoWang);
  alert(subobj.aproperty);
  alert(subobj.amethod());複製程式碼


還是不行。嗯嗯嗯,這次應該是父類的建構函式裡面this指標又指錯了。如果你沒猜到,請 ⬅到我之前的文章有講。


正如上面提到的文章裡面說的,要修改這個問題,我們可以:

var tmpf = LaoWang.bind(this);
tmpf();複製程式碼

也可以:

LaoWang.apply(this);複製程式碼

我用第二種,因為可以少寫一行程式碼:

var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
++  LaoWang.apply(this);
  };

  XiaoMing.prototype = LaoWang.prototype;
  XiaoMing.prototype.constructor = XiaoMing;
  var subobj = new XiaoMing();

  alert(subobj instanceof LaoWang);
  alert(subobj.aproperty);
  alert(subobj.amethod());複製程式碼

執行結果:

  true
  a
  b複製程式碼

搞定了!嗎?

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
    LaoWang.apply(this);
  };

  XiaoMing.prototype = LaoWang.prototype;
  XiaoMing.prototype.constructor = XiaoMing;

++XiaoMing.prototype.another_property = 'c';
  var subobj = new XiaoMing();
++var superobj = new LaoWang();

++alert(subobj.another_property);
++alert(superobj.another_property);複製程式碼

執行結果:

c
c複製程式碼

事實上我們剛才搞出來的完全不是繼承,而是其它不存在的東西。我姑且叫他“結對”吧。

方向完全錯了。。。雖然方向錯了,但其實大部分的努力還是正確的。正如我有時聽到一類故事,主角雖然創業失敗了,但是某一天發現,這個看似失敗的經歷積累的經驗和資源直接關係到後面的成功。所以我們繼續。

原型解耦

正常的做法是用兩個實體,一箇中間類(其實也是一個物件,第一等)和一箇中間物件(普通),來解耦我們剛才寫出來不知所謂的東西,關係我畫一下:

Subclass.prototype
          |---intermediate object
               |---.__proto__
                     |---IntermediateClass.prototype === Superclass.prototype複製程式碼

在這個裡面,中間物件是這個中間類的一個例項。

那我們回到上篇最開始的一段程式碼:

function inherits(ChildClass, ParentClass) {
  function IntermediateClass() {}
  IntermediateClass.prototype = ParentClass.prototype;
  ChildClass.prototype = new IntermediateClass;
  ChildClass.prototype.constructor = ChildClass;
  return ChildClass;
};複製程式碼

在看過具體問題和原理圖以後,是不是就讀得懂了呢?我們驗證一下:

  var LaoWang = function() {
    this.aproperty = 'a';
    this.amethod = function () {return 'b';};
  };

  var XiaoMing = function() {
    LaoWang.apply(this);
  };

--XiaoMing.prototype = LaoWang.prototype;
++inherits(XiaoMing, LaoWang);
  XiaoMing.prototype.another_property = 'c';
  var subobj = new XiaoMing();
  var superobj = new LaoWang();
  
  alert(subobj instanceof LaoWang);
  alert(subobj instanceof XiaoMing);
  alert(subobj.aproperty);
  alert(subobj.amethod());
  alert(subobj.another_property);
  alert(superobj instanceof LaoWang);
  alert(superobj instanceof XiaoMing);
  alert(superobj.aproperty);
  alert(superobj.amethod());
  alert(superobj.another_property);複製程式碼

事實上,一個更好版本的inherits() 可以用Object.create() 來實現:

function inherits(ChildClass, ParentClass) {
--  function IntermediateClass() {}
--  IntermediateClass.prototype = ParentClass.prototype;
--  ChildClass.prototype = new IntermediateClass;
++  ChildClass.prototype = Object.create(ParentClass.prototype);
    ChildClass.prototype.constructor = ChildClass;
    return ChildClass;
};複製程式碼

寫了這麼多,驗證的時候還有點小緊張:)

true
true
a
b
c
// this line is artificial
true
false
a
b
undefined複製程式碼

好了,可以按時下班了!

老鄉留步,還有最後一點沒說:

prototype.constructor

你可能已經留意到了,我在這裡偷偷加了一段程式碼但是沒說明。我是故意的,因為可以把上面的邏輯搞流暢一點。下面我們來看看這個。。。好吧,一言難盡,我再畫個圖。

class <----------------|
     |------.prototype.constructor
             |------......(I have drawn this part)複製程式碼

prototype.constructor其實是一個特殊的方法(最後再囉嗦一遍,第一類物件),這個方法指向的是這個類本身。正常情況下這個指向是由JS引擎在執行時完成的:

var LaoWang = function() {
  this.aproperty = 'a';
  this.amethod = function () {return 'b';};
};


alert(LaoWang.prototype.constructor === LaoWang);複製程式碼

執行結果:

true

但是呢,我在這個函式

function inherits(ChildClass, ParentClass)
複製程式碼

裡面做原型鏈的時候,把

ChildClass.prototype.constructor

這個指標的原始值給弄亂了,所以我們需要把這個值給設回去:

ChildClass.prototype.constructor = ChildClass;複製程式碼

好了,今天先寫到這。如果您覺得這篇不錯,可以關注我的公眾號 - 全棧獵人。

給程式設計師看的Javascript攻略 - Prototype (下)

也可以去Medium上隨意啪啪啪我的其他文章。感謝閱讀!?


相關文章