引入
在傳統的基於Class的語言如Java、C++中,繼承的本質是擴展一個已有的Class,並生成新的Subclass
由於這類語言嚴格區分類和實例,繼承實際上是類型的擴展
但是,JavaScript由於採用原型繼承,我們無法直接擴展一個 Class,因爲根本不存在Class這種類型, 那麼該怎麼辦呢?
我們先回顧Student構造函數
function Student(stdt) {
this.name = stdt.name;
Student.prototype.hello = function () { alert('Hello, ' + this.name + '!'); }
}
之後我們創建實例最好就傳入對象,這樣我們自己也不用考慮順序,也好操作
現在,我們要基於Student擴展出PrimaryStudent,可以先定義出PrimaryStudent
function PrimaryStudent(stdt) {
Student.call(this, stdt);
this.grade = stdt.grade || 1; }
call的用法直接也講過啦,這裏在簡單提一下,
f.call(this,x,y,z)
this是我們要綁定的對象,x,y,z是我們f的參數,這裏我們參數只有一個對象,於是就只傳入stdt
但是,調用了Student構造函數不等於繼承了Student
額這個是函數的原型鏈
PrimaryStudent創建的對象的原型鏈
new PrimaryStudent() ----> PrimaryStudent.prototype ----> Object.prototype ----> null
不過之後我們還是主要關注實例的原型鏈,因爲最終我們需要的其實是一個個實例
在js種,創建實例其實是通過函數生成的,一個個函數,我們既可以當他們是一個個具體的函數實例,比如說Math.abs,它可以計算,這個時候就是一個具體的實例了,也可以當成是一個類,比如說這裏的Student,我們不會去調用它,但我們會利用它擴展生成一類實例
因爲我們是通過Student擴展出的PrimaryStudent,所以我們希望原型鏈爲
xiaoming ----> PrimaryStudent.prototype ----> Student.prototype ----> Object.prototype ----> null
這樣,原型鏈對了,繼承關係就對了,新的基於Primary Student創建的對象不但能調用PrimaryStudent.prototype定義的方法,也可以調用Student.prototype定義的方法 ,這纔是我們想要的繼承
這樣的話其實也可以解決我們的代碼負擔,免得還要把方法再寫一遍
解決方法
第一種
因爲我們PrimaryStudent.prototype指向的是Object.prototype我們修改一下
我們只需要把PrimaryStudent.prototype的上一級改成Student.prototype就好了呀
而且也不影響函數的原型鏈
第二種(也是老的方法)
我們可以藉助一箇中間對象來實現正確的原型鏈,這個中間對象的原型要指向Student.prototype,爲了實現這一點,中間對象可以用一個空函數F來實現
首先看看如果一個函數修改了prototype會有什麼變化
(最下面灰色的是函數的原型鏈,跟我們這裏沒關係,而且我看了,確實也不影響)
修改了prototype之後的F果然是大變活人呀,真的一摸一樣
剛纔第一種我是這樣修改的,沒啥問題
function F(){};
F.prototype = Student.prototype;
但如果定義在了函數內部,記着要調用一下自己喲
好啦,下面開始我們繼承啦
function PrimaryStudent(stdt) {
Student.call(this, stdt);
this.grade = stdt.grade || 1;
}
function F() { };//因爲怕自己忘記調用了,還是我們手動修改
F.prototype = Student.prototype;
PrimaryStudent.prototype = new F();
這樣繼承不知道能不能理解
之前我們的new F()是用來創建實例,這裏我們把new F()創建的實例賦值給了PrimaryStudent,其實也不要把new F()想狹隘了,new F()就是給你創造一個空的對象,然後裏面給你加個繼承關係,剛好適合我們這裏的prototype
(這是原來的PrimartStudent)
這裏獲得好了繼承關係後,但是我們沒有構造函數了,因爲new的作用機理,給的是空對象和繼承關係,把你原來的prototype裏的內容清空了,所以我們自己需要把構造函數補上
PrimaryStudent.prototype.constructor = PrimaryStudent;
跟修改之前看起來差不多,但其實繼承關係變啦
第三種方法
只是把第二種方法變成個函數
如果把繼承這個動作用一個inherits()函數封裝起來,還可以隱藏F的定義,並簡化代碼:
function inherits(Child, Parent) {
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
}
這個inherits()函數可以複用
小結
JavaScript的原型繼承實現方式就是
- 定義新的構造函數,並在內部用call()調用希望“繼承”的構造函數,並綁定this;
- 藉助中間函數F實現原型鏈繼承,最好通過封裝的inherits函數完成;
- 繼續在新的構造函數的原型上定義新方法
- 定義prototype裏的方法和屬性是可以在函數內部,但是如果是直接修改prototype爲另一個函數的,最好是在外面,免得忘記執行了(方法和屬性因爲實例化的時候new已經主動調用了,所以不影響)
小小疑惑
後來我又發現,我們這一步沒什麼效果
PrimaryStudent.prototype.constructor = PrimaryStudent;
這個時候相當於我們prototype下面就沒有構造函數
其實兩個真的差不多,難道constructor真的是個擺設嗎
我們來測試一下
把整個prototype改空後,運行不了了
只修改了constructor
正常情況
兩種好像真的差不多,額所以這裏…我個人覺得吧可能constructor只是爲了方便我們…,沒什麼真實作用
而且還有一點,就是prototype下定義的變量,函數,如果有新創建對象,一定要注意,所有的值會重新被初始化,因爲我們new Student調用了Student,他又會執行一遍賦值的活動,所以…這一點也要注意一下,(因爲所有實例都是訪問它,所以大家跟着一起變)
但是實例變量就沒這個問題了,可以認爲是無法修改的,因爲你在函數內部你的this綁定的是window(undefined)你怎麼也訪問不到,所以可以實例變量和方法是無法被改變的,除非你修改實例對象自己的,但還是跟構建函數無關