1、前言前端
這篇開始主要介紹代碼複用模式(原書中的第六章),任何一位有理想的開發者都不肯意將一樣的邏輯代碼重寫屢次,複用也是提高本身開發能力中重要的一環,因此本篇也將從「繼承」開始,聊聊開發中的各類代碼複用模式。java
其實在上一章,我感受這本書後面不少東西是我不太理解的,但我仍是想堅持讀完,在之後知識逐漸積累,我會回頭來完善這些概念,算是給之前的本身答疑解惑。app
2、類式繼承VS現代繼承模式函數
1.什麼是類式繼承性能
談到類式繼承或者類classical,你們都有所耳聞,例如在java中,每一個對象都是一個指定類的實例,且一個對象不能在不存在對應類的狀況下存在。學習
而在JS中其實並無原生的類的概念,且JS的對象均可以隨意的建立修改,並不須要依賴類。若是真要說,JS也有與類類似的構造函數,其語法也是經過new運算符獲得一個實例。this
假設工廠要生產一批杯子,接到的圖紙信息是,杯子高12cm,杯口直徑8cm,按照常識,咱們不可能按照信息一個個的去作,最好的方法是直接作一個模具出來,而後灌漿批量生產。spa
這裏每一個杯子就是一個對象,一個實例,而這個提早定義好杯子信息的模具就是一個「類」,經過這個模具(類),咱們就能夠快速生產多個繼承了模具信息(高度,直徑等)的杯子(實例)了。prototype
//不合理作法 let cup1 = { height:12, diameter:8 }; let cup2 = { height:12, diameter:8 }; // ...... let cupn1000= { height:12, diameter:8 }; //構造函數的作法 function MakeCup () { this.length = 12, this.diameter = 8; }; let cup1 = new MakeCup(); let cup2 = new MakeCup(); //......... let cup1000 = new MakeCup();
在上述代碼中,MakeCup就是一個包含了全部實例共有信息的「類」,固然在JS中,咱們更喜歡將這個類稱爲構造函數,畢竟MakeCup只是一個函數,而這種作法也只是與類類似,在這裏咱們將這種實現方式稱爲「類式繼承」。code
雖然咱們在討論類式繼承,但仍是儘可能避免使用類這個字,在JS中構造函數或者constructor更爲精準,畢竟每一個人對於類的理解可能不一樣,將類與構造函數混合在一塊兒容易混淆。
2.1類式繼承1--默認模式(借用原型)
如今有下面兩個構造函數Child()與Parent(),要求是經過Child來建立一個實例,而且這個實例要得到構造函數Parent的屬性。咱們假設經過inherit函數實現了需求。
function Parent(name) { this.name = name || 'Adam'; }; Parent.prototype.say = function () { console.log(this.name); }; //空的child構造函數 function Child(name) {}; //繼承 inherit(Child, Parent);
那麼這個inherit函數如何實現,第一種思路,咱們經過new Parent()獲得一個實例,而後將Child函數的prototype指向該實例。
function inherit(C, P) { C.prototype = new P(); } inherit(Child, Parent); let kid = new Child(); kid.say();//Adam
很明顯,構造函數Child繼承了構造函數Parent的屬性,因此由構造函數Child建立的實例天然也繼承了這些屬性,那麼這個過程當中間到底發生了什麼?咱們嘗試跟蹤原型鏈。
提早說明,爲了方便理解,咱們就假設對象啊,原型啊,都在同一空間內,當咱們new Parent()時,就獲得了一個實例,此時在內存中也新開了一個空間存放這個實例(下圖中的2區域)。
構造函數Parent的原型鏈
如今咱們嘗試訪問say()方法,可是2號空間並無這個方法,可是經過_proto_指向Parent構造函數的prototype屬性時,竟然能夠訪問這個方法(1區域),這也是爲何咱們總在前面說,建議將全部實例都須要用到的屬性添加在prototype上,由於這樣在每次new時,不用每次新開內存時都建立一次。
咱們再來看看在使用inherit函數後,再使用let kid = new Child()建立實例時發生了什麼,以下圖。
繼承以後的原型鏈
一開始Child構造函數是空的,什麼屬性都沒有(上圖1區域),當inherit函數執行時,Child函數的prototype屬性指向了new Parent()對象,也就是2區域。
當咱們new Child()獲得一個實例kid並使用say方法時,因爲自身沒有,只能順着_proto_找到了new Parent(),結果此對象也沒有,重複了咱們上面的圖解步驟,繼續順着_proto_找到了Parent.prototype,終於找到了say()方法。
當say()方法被調用時,咱們輸出this.name,而此時this指向的是new Child(),結果new Child()又沒有這個name屬性,跟say同樣,再找到2,再到1區域,順利輸出了Adam。這樣是否是很清晰了呢?
咱們再來爲實例kid加點屬性,看看原型鏈的變化,以下圖
let kid = new Child(); kid.name = 'Patrick' kid.say();//Patrick
繼承並給實例添加屬性後的原型鏈
當咱們爲實例添加了name屬性時,其實只是爲new Child()添加了name屬性(區域3),並不會影響到new Parent(),這也是爲何說,每一個實例都是一個獨立的個體。當咱們再次尋找say()方法時,仍是同樣的順着_proto_找到了Parent.prototype,而當咱們調用say方法輸出name屬性時,因爲當前this指向kid,且kid本身有了name屬性,因而順利輸出了Patrick。
而當咱們delete kid.name刪除掉以前賦值的Patrick時,再次調用,能夠發現又輸出了Adam,因此原型鏈繼承就是,先從本身身上找,找不到,順着_proto_向上,直至找到null中止(原型鏈的頂端是null)。
2.1原型鏈的優勢與弊端
原型鏈繼承的壞處在於,在繼承父對象中你想要的屬性的同時,也會繼承父對象你不想要的屬性,好比上方代碼,我只想要父對象原型鏈上的say方法,結果你仍是把構造函數中的name屬性打包給我了。
上面這種模式的第二個壞處是,我不能給我最終的實例kid傳遞形參,假設我想最終輸出時間跳躍,要麼kid.name = ‘時間跳躍’,要麼在父構造函數時就傳遞好參數Parent(‘時間跳躍’)。但這樣咱們得不停的修改Parent對象。
let kid = new Child('時間跳躍'); kid.say();//Adan
但若是一個屬性或方法須要複用,它仍是應該被添加在構造函數的原型prototype上;兩點理由,第一,加在原型鏈上,new實例時不須要反覆建立屬性形成內存浪費,第二,簡化構造函數的屬性能減輕對不須要這些屬性的實例的困擾,這也是原型鏈繼承的好處。
3.類式繼承2---借用構造函數
咱們在上個例子中,遇到了沒法經過子對象傳參到父對象的問題,咱們修改Child構造函數,以下,就能夠實現子對象傳參了。
function Child(a, b, c, d) { Parent.apply(this, arguments); }; let kid = new Child('時間跳躍'); console.log(kid.name);//時間跳躍
實現原理很簡單,當咱們new Child()時,經過apply再次應用了Parent函數,但Parent執行時此時的this指向了Child,也就是說Child想有name屬性,但是我沒有this.name的賦值操做,因而經過apply改變this的原理,借用了Parent函數中的this.name = name || 'Adam'這句代碼,變相的來爲Child構造函數添加屬性,它等同於Child.name = '時間跳躍' || 'Adam'。
注意,此處只是借用這句代碼來爲Child構造函數添加屬性,並無修改Parent構造函數的屬性,咱們嘗試輸出Parent的實例,能夠發現name屬性仍爲Adam。
let parent = new Parent(); let kid = new Child('時間跳躍'); console.log(kid, parent);//時間跳躍 Adam
咱們在上面原型鏈的例子中,Child的實例去繼承Parent的屬性,說是繼承,實際上是經過原型鏈去找,雖然能拿到,但本質上這個屬性仍是別人的,本身手裏沒有,哪天Parent心情很差,把name屬性給刪了,Child啃老的行爲也基本到頭了。
但下面Child構造函數中使用apply的作法就不一樣了,我直接借用Parent的代碼來爲本身添加只屬於本身的name屬性,管你Parent怎麼操做name屬性,都跟我不相關。若是說第一種繼承是引用,那麼這種作法就更像是複製,我複製你有的屬性,就不用引用了。
有點授人以魚不如授人以漁的寓意,也有點深淺拷貝的意思。
我稍微修改了上面的代碼,使用原型鏈指向繼承獲得了實例kid與使用call複製屬性獲得的實例son,分別輸出了它們的hasOwnProperty判斷,這裏答案應該能明白了。
function Parent(name) { this.name = ["echo", "時間跳躍", "聽風是風"]; }; Parent.prototype.say = function() { console.log(this.name); }; //獲得一個實例 let parent = new Parent(); function Child() {}; //修改Chilkd的原型指向 Child.prototype = parent; function Son() { Parent.call(this); }; let kid = new Child(); let son = new Son(); console.log(parent.hasOwnProperty('name'));//true console.log(kid.hasOwnProperty('name'));//false console.log(son.hasOwnProperty('name'));//true
照理說,實例parent與實例son的name屬性是自身的,不像kid這個沒骨氣的是靠引用地址借來的,咱們分別修改三個實例的name屬性,這段代碼是我本身改的,當出個題,看看下面三個console分別輸出什麼,學繼承,也當原型鏈的題來考考本身。
function Parent() { this.name = ["echo", "時間跳躍", "聽風是風"]; }; Parent.prototype.say = function() { console.log(this.name); }; let parent = new Parent(); function Child() {}; Child.prototype = parent; let kid = new Child(); function Son() { Parent.call(this); }; let son = new Son(); parent.name.push('二狗子'); son.name.push('狗剩'); kid.name.push('狗蛋'); console.log(parent.name);//? let parent1 = new Parent(); let kid1 = new Child(); console.log(parent1.name);//? console.log(kid1.name);//?
有沒有以爲使用call或者apply的構造函數方式很厲害,但這種模式也有本身的弊端,雖然它借用了父構造函數的屬性建立代碼,很遺憾它並沒辦法繼承父構造函數的prototype屬性。咱們寫個簡單的例子:
function Parent(name) { this.name = name || "Adam"; }; Parent.prototype.say = function () { console.log(this.name); }; function Child (name) { Parent.apply(this,arguments); }; let kid = new Child('Patrick'); console.log(kid)//undefined
跟上面同樣,咱們經過原型圖來看看這段代碼繼承關係。
儘管咱們經過改變this指向爲kid建立了name屬性,但當找say方法時,因爲此時的this指向Child,而Child的prototype並無提供這個方法,因此沒法找到。
3.1利用構造函數模式實現多繼承
利用構造函數加apply的方式,咱們能夠同時繼承多個構造函數的屬性,像這樣:
function Cat () { this.legs = 4; this.say = function () { console.log('喵~') } }; function Bird() { this.wings = 2; this.fly = true; } function CatWings() { Cat.apply(this); Bird.apply(this); }; let miao = new CatWings(); console.dir(miao);
簡直不能在方便,那麼到這裏位置,咱們大概介紹了類式繼承,默認模式,也就是構造函數的property指向你須要繼承的實例,構造函數模式(結合call或apply)。
第二種構造函數模式的弊端在於不能繼承原型,而添加在原型上的每每又是可複用的方法,這點比較遺憾。
但它也有好處,例如它能得到父對象成員的拷貝,不存在子對象修改能影響父對象的風險。那麼這個遺憾咱們能不能解決呢,若是在構造函數的模式上繼承原型呢。下面的一種模式來解決這個問題。
JS模式這本書我可能最近,至少一週須要放放了,昨天跟組長說咱們如今前端ES6規範都沒用,確實low了點,因此我這邊想盡快把ES6實踐到項目中,這幾天打算把ES6過一遍,因此想寫寫ES6的筆記。反正無論學什麼,只要願意學,老是沒壞處的。
我爲何要寫這段話呢,說的像我有不少讀者,要提早說明同樣。其實根本沒人看個人博客啊...
那麼這篇就寫到這裏了,接下來先放置一下,這本書還剩下兩章,我會堅持讀完,接下來好好學習一下ES6,爲四月項目重構作準備。