本文是學習中傳思客在慕課網開的課程《前端跳槽面試必備技巧》的學習筆記。課程地址:https://coding.imooc.com/class/evaluation/129.html#Anchor。
本文將從以下幾方面介紹類與繼承
- 類的聲明與實例化
- 如何實現繼承
- 繼承的幾種方式
類的聲明與實例化
類的聲明一般有兩種方式
//類的聲明 var Animal = function () { this.name = 'Animal'; }; //ES6中類的聲明 class Animal2 { constructor () { this.name = 'Animal2'; } }
實例化就比較簡單,直接用new運算符
new Animall() new Animal2()
這些比較簡單,簡單介紹一下就可以了。接下來,介紹本文的重點內容,繼承。
如何實現繼承
實現繼承的方式主要有兩種:
第一種借助構造函數實現繼承
先看個了例子
function Parent1 () { this.name = 'parent1'; } function Child1 () { Parent1.call(this); //這里的call用apply也可以 this.type = 'child1'; } console.log(new Child1());
輸出結果
可以看到,生成Child1里面有了父級的屬性name,實現了繼承。為什么就實現繼承了呢?
因為在Child1里執行了這句 Parent1.call(this); 如果對this不理解的話,建議看看這個JavaScript作用域和閉包
在子類的函數體里執行父級的構造函數,同時改變函數運行的上下文環境(也就是this的指向),使this指向Child1這個類,從而導致了父類的屬性都會掛載到子類這個類上去,如此便實現了繼承。
但這種繼承的方法有一個缺點,它只是把父類中的屬性繼承了,但父類的原型中的屬性繼承不了。繼續上面的代碼
Parent1.prototype.say = function () { console.log("Parent1 prototype") }; new Child1().say()
從結果中可以看出 Child1中是沒有say方法的,因為say是加在父類的原型上的,這種繼承方式只改變父類構造函數在子類函數體中的指向,繼承不了原型的屬性。
第二種是借助原型鏈實現繼承
原型鏈這里直接用了,不再詳細介紹了,如果對原型鏈還不是很了解的話,建議先看看這個,詳談Javascript原型鏈
function Parent2 () { this.name = 'parent2'; this.play = [1, 2, 3]; } function Child2 () { this.type = 'child2'; } Child2.prototype = new Parent2(); //通過把Child2的原型指向Parent2來實現繼承
在瀏覽器中檢驗一下
可以看到在Child2的實例的__proto__的屬性中有Parent2的屬性,由此實現了Child2從Parent2的繼承。
但這種繼承方式也有不足。接着看代碼
var s1 = new Child2(); var s2 = new Child2(); s1.play.push(4);
console.log('s1.play:'+s1.play);
console.log('s2.play:'+s2.play);
打印結果
我們只改了s1這個實例的屬性,卻發現Child2的其他實例的屬性都一起改變了,因為s1修改的是它原型的屬性,原型的屬性修改,所有繼承自該原型的類的屬性都會一起改變,因此Child2的實例之間並沒有隔離開來,這顯然不是我們想要的。
第三種 組合方式
組合方式就是前兩種方法組合而成的,上面兩種方式都有不足,這種方式就解決了上面兩種方式的不足。
看代碼
function Parent3 () { this.name = 'parent3'; this.play = [1, 2, 3]; } function Child3 () { Parent3.call(this); //子類里執行父類構造函數 this.type = 'child3'; } Child3.prototype = new Parent3(); //子類的原型指向父類 //以下是測試代碼 var s3 = new Child3(); var s4 = new Child3(); s3.play.push(4); console.log(s3.play, s4.play);
打印結果
可以看出,修改某個實例的屬性,並不會引起父類的屬性的變化。
這種方式的繼承把構造函數和原型鏈的繼承的方式的優點結合起來,並彌補了二者的不足,功能上已經沒有缺點了。
但這種方法仍不完美,因為創建一個子類的實例的時候,父類的構造函數執行了兩次。
每一次創建實例,都會執行兩次構造函數這是沒有必要的,因為在繼承構造函數的時侯,也就是Parent3.call(this)的時候,parnet的屬性已經在child里運行了,外面原型鏈繼承的時候就沒有必要再執行一次了。所以,接下來我們對這一方法再做一個優化。
第四種 組合方式的優化
上面一種繼承方式問題出在繼承原型的時候又一次執行了父類的構造函數,所以優化就從這一點出發。
組合方式中為了解決借助構造函數繼承(也就是本文中第一種)的缺點,父類的原型中的屬性繼承不了,所以才把子類的原型指向了父類。
但是父類的屬性,在子類已經中已經存在了,子類只是缺少父類的原型中的屬性,所以,根據這一點,我們做出優化。
function Parent4 () { this.name = 'parent4'; this.play = [1, 2, 3]; } function Child4 () { Parent4.call(this); this.type = 'child4'; } Child4.prototype = Parent4.prototype; //優化的點在這里 //以下為測試代碼 var s5 = new Child4(); var s6 = new Child4(); console.log(s5, s6); console.log(s5 instanceof Child4, s5 instanceof Parent4); console.log(s5.constructor);
在這種繼承方式中,並沒有把直接把子類的原型指向父類,而是指向了父類的原型。這樣就避免了父類構造函數的二次執行,從而完成了針對組合方式的優化。但還是有一點小問題,先看輸出結果
可以看到s5是new Child4()出來的,但是他的constructor卻是Parent4.
這是因為Child4這個類中並沒有構造函數,它的構造函數是從原型鏈中的上一級拿過來的,也就是Parent4。所以說到這里,終於能把最完美的繼承方式接受給大家啦。
接下來。。。
第五種 組合的完美優化
先看代碼吧
function Parent5 () { this.name = 'parent5'; this.play = [1, 2, 3]; } function Child5 () { Parent5.call(this); this.type = 'child5'; } //把子類的原型指向通過Object.create創建的中間對象 Child5.prototype = Object.create(Parent5.prototype); //把Child5的原型的構造函數指向自己 Child5.prototype.constructor = Child5; //測試 var s7= new Child5(); console.log(s7 instanceof Child5, s7 instanceof Parent5) console.log(s7.constructor);
本例中通過把子類的原型指向Object.create(Parent5.prototype),實現了子類和父類構造函數的分離,但是這時子類中還是沒有自己的構造函數,所以緊接着又設置了子類的構造函數,由此實現了完美的組合繼承。
測試結果
總結:
本文並沒有直接把最完美的繼承直接寫出來,而是由淺入深循序漸進的來介紹的,如果對后面幾種方法沒看太懂的話,可能是原型鏈掌握的不夠好,還是建議看看這個詳談Javascript原型鏈。
類的繼承就告一段落了,這部分內容確實不好理解,文章寫起來也不好寫,可能有的地方語言組織的也不好,有點難懂。大家湊合着看,多看幾遍,敲一敲代碼就懂了。
如果覺得本文對你有幫助就點個贊吧^_^