再和“面向對象”談戀愛 - 繼承(五)

通過上一篇文章想必各位老鐵已經熟悉了class了,這篇文章接着介紹繼承。面向對象裏最大的特點應該就屬繼承了。一個項目可能需要不斷的迭代、完善、升級。那每一次的更新你是要重新寫呢,還是在原有的基礎上改吧改吧呢?當然,不是缺心眼的人肯定都會在原來的基礎上改吧改吧,那這個改吧改吧就需要用到繼承了。

第二篇文章裏說過原型實例跟構造函數之間的繼承,並且還講了一道推算題。最終我們明白,實例爲什麼能繼承原型上的內容是因爲prototype,所以在ES5裏面想要繼承的話就得通過原型,需要對prototype進行一頓蹂躪才行。那到了ES6裏面一切就簡單了,像開了掛似的!so easy,哪裏不會點哪裏!

繼承

  • class類可以通過extends實現繼承
  • 利用super關鍵字引入父類的構造函數
  • ES6規定子類必需在構造函數(constructor)裏先調用super方法
  • 子類能同時繼承父類的共享方法與私有方法
//這個類做爲父類('老王')
class OldWang{   
    constructor(work,money){
        this.work=work;
        this.money=money;
    }
    showWork(){
        console.log(`老王是個${this.work},看了我的文章後,能力達到了${this.level},一個月能掙${this.money}元`);
    }
    static play(){  //這是個私有方法,但子類依然能繼承到
        console.log('大吉大利,今晚吃雞!不會玩遊戲的前端不是個好前端!');
    }
}

//子類繼承父類
class SmallWang extends OldWang{   
    constructor(work,money,level){
        //這裏必需先寫super,不然會報錯
        super(work,money,level);
        this.level=level;   //只有用了super,才能使用this
    }
}

//生成實例
const wang=new SmallWang('前端',20000,'T5');
wang.showWork();    //老王是個前端,看了我的文章後,能力達到了T5,一個月能掙20000元
SmallWang.play();   //大吉大利,今晚吃雞!不會玩遊戲的前端不是個好前端!  子類能繼承父類的私有方法

//與ES5裏的實例是一致的
console.log(
    Object.getPrototypeOf(SmallWang)===OldWang, //true 子類的原型是OldWang,也就是說,它是OldWang的實例
    wang instanceof OldWang,        //true
    wang instanceof SmallWang,      //true
);

ES5的繼承,實質是先聲明子類,然後通過call方法將父類的方法添加到子類上,而ES6的繼承機制完全不同。實質是聲明瞭子類後,子類並沒有this對象,而是利用super方法引入父類的this對象,再將this修改成子類,就這麼神奇!

new.target

new是生成實例的命令。ES6new命令引入了一個new.target屬性,該屬性一般用在構造函數之中

  • new.target返回new命令作用於的那個類
  • 子類繼承父類時,new.target返回子類
class Person{
    constructor(){
        //如果類不是通過new調用的,就會返回undefined
        if(new.target===undefined){
            throw new Error('請使用new生成實例!');
        }
        console.log(new.target.name);
    }
}
new Person();   //Person類(返回了new作用於的那個類)
Person();       //有些瀏覽器可以不帶new生成實例,就會拋出一個錯誤

class Man extends Person{
}
new Man();  //Man(子類繼承父類時,new.target會返回子類)

//利用這個特性實現一個不能獨立使用,必需繼承後才能用的類(像React裏的組件)
class Uncle{
    constructor(){
        if(new.target===Uncle){
            throw new Error('這個類不能實例化,只能繼承後再用');
        }
    }
    showUncle(){
        console.log('都是他舅');
    }
}
//new Uncle();  報錯

//通過繼承就可以使用Uncle了
class BigUncle extends Uncle{
    constructor(){
        super();    //引入父類的構造函數,必須加不然報錯
        this.uncle='他大舅';
    }
}

//實例
const uncle=new BigUncle();
uncle.showUncle();  //都是他舅

原型

class裏的原型關係相對於ES5裏的原型關係,ES6對其進行了修改,但只修改了子類與父類之間的關係,其它的關係並沒有修改。

  1. 子類的__proto__,表示構造函數的繼承,指向父類構造函數
  2. 子類prototype屬性的__proto__,表示方法的繼承,指向父類的prototype

ES5裏的繼承關係,在第二篇文章裏詳細介紹過,再回顧一下:

//ES5的繼承關係
const str=new String(123);
console.log(
    str.__proto__===String.prototype,       //true
    String.__proto__===Function.prototype   //true
);  

//可以看到不管實例還是構造函數,它們的__proto__屬性永遠都指向原型

ES6與ES5的對比如下:

//ES5
function Ball(){}
function Football(){
    Ball.call(this);    //ES5的繼承
}

//ES6
class Father{};
class Son extends Father{}

//構造函數,關係沒變
console.log(
    '構造函數',
    Ball.__proto__===Ball.prototype,    //false
    Father.__proto__===Father.prototype,//false

    Ball.__proto__===Function.prototype,    //true
    Father.__proto__===Function.prototype   //true
);

//實例,關係沒變
console.log(
    '實例',
    new Ball().__proto__===Ball.prototype,      //true
    new Father().__proto__===Father.prototype   //true
);

//子類,關係變了
console.log(
    '子類的__proto__',
    Football.__proto__===Ball,  //false ES5
    Football.__proto__===Function.prototype,//true  ES5

    Son.__proto__===Father,     //true ES6
    Son.__proto__===Father.prototype,   //false ES6

    //ES6的變化爲:子類的__proto__指向父類
);

console.log(
    '子類的prototype的__proto__屬性',
    Football.prototype.__proto__===Ball.prototype,  //false ES5
    Football.prototype.__proto__===Object.prototype,//true  ESS

    Son.prototype.__proto__===Object.prototype,     //false ES6
    Son.prototype.__proto__===Father.prototype,     //true ES6

    //ES6的變化爲:子類的prototype的__proto__屬性指向父類的prototype
);

由此可以看出ES6只修改了子類跟父類間的原型關係,其它的不受影響。那至於ES6對這兩條關係做了修改的原因跟ES6的繼承機制有關係,ES6內部的繼承用的是Object.setPrototypeOf方法(ES6新增的方法,作用是把第一個參數的原型設置成第二個參數),以下爲內部過程:

{
    class Father{};
    class Son{};

    //son的實例繼承Father的實例,內部會執行下面的代碼
    Object.setPrototypeOf(Son.prototype,Father.prototype);
    //等同於Son.prototype.__proto__=Father.prototype;所以得出結果:子類prototype屬性的__proto__屬性,表示方法的繼承,指向父類的prototype屬性

    //son繼承Father的私有屬性,內部會執行下面的代碼
    Object.setPrototypeOf(Son,Father);
    //等同於Son.__proto__=Father;所以得出結果:子類的__proto__屬性,表示構造函數的繼承,指向父類
}

爲什麼用了setPrototypeOf後,等價於把第一個參數的__proto__的值設置成第二個參數?是因爲setPrototypeOf方法的內部是這樣的:

//setPrototypeOf方法內部主要代碼
Object.setPrototypeOf=function(obj,proto){
    obj.__proto__=proto;
    return obj;
}

下一篇介紹super關鍵字

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章