JS中的繼承(上)
學過java或者c#之類語言的同學,應該會對js的繼承感到很困惑--不要問我怎麼知道的,js的繼承主要是基於原型(prototype)的,對js的原型感興趣的同學, 可以瞭解一下我之前寫的JS中的原型物件
相信很多同學也跟我一樣,剛開始接觸js的物件導向程式設計的時候,都抱著一種排斥的心態--為什麼js這麼麻煩? 其實瞭解完原型鏈後,再來看js的繼承,你會發現js的繼承其實比其他OOP語言更簡單,更靈活,我們來看一個基於原型鏈的繼承
// 父類
function Person() {}
// 子類
function Student(){}
// 繼承
Student.prototype = new Person()
複製程式碼
我們只要把子類的prototype設定為父類的例項
,就完成了繼承,怎麼樣? 是不是超級簡單? 有沒有比Java,C#的清晰?
事實上,以上就是js裡面的原型鏈繼承
當然,通過以上程式碼,我們的Student只是繼承了一個空殼的Person,這樣視乎是毫無意義的,我們使用繼承的目的, 就是要通過繼承獲取父類的內容,那我們先給父類加上一點點簡單的內容(新增的地方標記 '// 新增的程式碼'):
// 父類
function Person(name,age) {
this.name = name || 'unknow' // 新增的程式碼
this.age = age || 0 // 新增的程式碼
}
// 子類
function Student(name){
this.name = name // 新增的程式碼
this.score = 80 // 新增的程式碼
}
// 繼承
Student.prototype = new Person()
複製程式碼
使用
var stu = new Student('lucy')
console.log(stu.name) // lucy --子類覆蓋父類的屬性
console.log(stu.age) // 0 --父類的屬性
console.log(stu.score) // 80 --子類自己的屬性
複製程式碼
這裡為了降低複雜度,我們只演示了普通屬性的繼承,沒有演示方法的繼承,事實上,方法的繼承也很簡單, 我們再來稍微修改一下程式碼,基於上面的程式碼,給父類和子類分別加一個方法(新增的地方標記 '// 新增的程式碼')
// 父類
function Person(name,age) {
this.name = name || 'unknow'
this.age = age || 0
}
// 為父類新曾一個方法
Person.prototype.say = function() { // 新增的程式碼
console.log('I am a person')
}
// 子類
function Student(name){
this.name = name
this.score = 80
}
// 繼承 注意,繼承必須要寫在子類方法定義的前面
Student.prototype = new Person()
// 為子類新增一個方法(在繼承之後,否則會被覆蓋)
Student.prototype.study = function () { // 新增的程式碼
console.log('I am studing')
}
複製程式碼
使用
var stu = new Student('lucy')
console.log(stu.name) // lucy --子類覆蓋父類的屬性
console.log(stu.age) // 0 --父類的屬性
console.log(stu.score) // 80 --子類自己的屬性
stu.say() // I am a person --繼承自父類的方法
stu.study() // I am studing --子類自己的方法
複製程式碼
這樣,看起來我們好像已經完成了一個完整的繼承了,這個就是原型鏈繼承,怎麼樣,是不是很好理解? 但是,原型鏈繼承有一個缺點,就是屬性如果是引用型別的話,會共享引用型別,請看以下程式碼
// 父類
function Person() {
this.hobbies = ['music','reading']
}
// 子類
function Student(){}
// 繼承
Student.prototype = new Person()
複製程式碼
使用
var stu1 = new Student()
var stu2 = new Student()
stu1.hobbies.push('basketball')
console.log(stu1.hobbies) // music,reading,basketball
console.log(stu2.hobbies) // music,reading,basketball
複製程式碼
我們可以看到,當我們改變stu1的引用型別的屬性時,stu2對應的屬性,也會跟著更改,這就是原型鏈繼承缺點 --引用屬性會被所有例項共享, 那我們如何解決這個問題呢? 就是下面我們要提到的借用建構函式繼承,我們來看一下使用建構函式繼承的最簡單例子:
// 父類
function Person() {
this.hobbies = ['music','reading']
}
// 子類
function Student(){
Person.call(this) // 新增的程式碼
}
複製程式碼
使用
var stu1 = new Student()
var stu2 = new Student()
stu1.hobbies.push('basketball')
console.log(stu1.hobbies) // music,reading,basketball
console.log(stu2.hobbies) // music,reading
複製程式碼
這樣,我們就解決了引用型別被所有例項共享的問題了
注意,這裡跟 原型鏈繼承 有個比較明顯的區別是並沒有使用prototype繼承,而是在子類裡面執行父類的建構函式, 相當於把父類的程式碼複製到子類裡面執行一遍,這樣做的另一個好處就是可以給父類傳參
// 父類
function Person(name) {
this.name = name // 新增的程式碼
}
// 子類
function Student(name){
Person.call(this,name) // 改動的程式碼
}
複製程式碼
使用
var stu1 = new Student('lucy')
var stu2 = new Student('lili')
console.log(stu1.name) // lucy
console.log(stu2.name) // lili
複製程式碼
看起來已經很有Java,C#的味道了有沒有?
但是,建構函式解決了引用型別被所有例項共享的問題,但正是因為解決了這個問題,導致一個很矛盾的問題出現了,--函式也是引用型別, 也沒辦法共享了.也就是說,每個例項裡面的函式,雖然功能一樣,但是卻不是同一個函式,就相當於我們每例項化一個子類,就複製了一遍的函式程式碼
// 父類
function Person(name) {
this.say = function() {} // 改動的程式碼
}
// 子類
function Student(name){
Person.call(this,name)
}
複製程式碼
使用
var stu1 = new Student('lucy')
var stu2 = new Student('lili')
console.log(stu1.say === stu2.say) // false
複製程式碼
以上程式碼可以證明,父類的函式,在子類的例項下是不共享的
總結
繼承方式 | 繼承核心程式碼 | 優缺點 |
---|---|---|
原型鏈繼承 | Student.prototype = new Person() |
例項的引用型別共享 |
建構函式繼承 | 在子類(Student)裡執行 Person.call(this) |
例項的引用型別不共享 |
從上表我們可以看出 原型鏈繼承
和 建構函式繼承
這兩種繼承方式的優缺點剛好是互相矛盾的,那麼我們有沒有辦法魚和熊掌兼得呢?
沒有的話,我就不會說出來了,^_^,接下來請允許我隆重介紹 組合繼承
組合繼承,就是各取上面2種繼承的長處,普通屬性 使用 建構函式繼承
,函式 使用 原型鏈繼承
,
這個程式碼稍微複雜一點,不過相信有了上面的基礎後,看起來也是很輕鬆
// 父類
function Person() {
this.hobbies = ['music','reading']
}
// 父類函式
Person.prototype.say = function() {console.log('I am a person')}
// 子類
function Student(){
Person.call(this) // 建構函式繼承(繼承屬性)
}
// 繼承
Student.prototype = new Person() // 原型鏈繼承(繼承方法)
複製程式碼
使用
// 例項化
var stu1 = new Student()
var stu2 = new Student()
stu1.hobbies.push('basketball')
console.log(stu1.hobbies) // music,reading,basketball
console.log(stu2.hobbies) // music,reading
console.log(stu1.say == stu2.say) // true
複製程式碼
這樣,我們就既能實現屬性的獨立,又能做到函式的共享,是不是很完美呢?
組合繼承據說是JavaScript中最常用的繼承方式(具體無法考證哈).
至此,我們就把js裡面的常用繼承瞭解完了,其實也沒有那麼難嘛!不過,我們總結一下3種繼承
- 原型鏈繼承,會共享引用屬性
- 建構函式繼承,會獨享所有屬性,包括引用屬性(重點是函式)
- 組合繼承,利用原型鏈繼承要共享的屬性,利用建構函式繼承要獨享的屬性,實現相對完美的繼承
上面為什麼要說相對完美呢? 因為本文的標題叫【JS中的繼承(上)】,那肯定是還有 【JS中的繼承(下)】咯, 目前為止,我們只講了3種最基本的繼承,事實上,JavaScript還有好多繼承方式,為了讓你不至於學習疲勞,所以我打算分開來講, 如果你沒有那個耐性繼續看下去,那麼看完這篇對於理解JavaScript的繼承,也是夠用的。但是建議多看兩遍,加深印象, 我學js繼承的時候,那本犀牛書都被我翻爛了,寫這篇文字的時候,我還在一遍翻一邊寫的呢(噓!)
好了,今天就到這裡,感謝收看,如果覺得對您有用,請給本文的github加個star,萬分感謝, 另外,github上還有其他一些關於前端的教程和元件,有興趣的童鞋可以看看,你們的支援就是我最大的動力。