原文發表在: 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;複製程式碼
好了,今天先寫到這。如果您覺得這篇不錯,可以關注我的公眾號 - 全棧獵人。
也可以去Medium上隨意啪啪啪我的其他文章。感謝閱讀!?