精讀JavaScript模式(八),JS類式繼承

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,爲四月項目重構作準備。

相關文章
相關標籤/搜索