JavaScript 繼承全解析

文叔叔發表於2019-05-29

ES5的繼承方式

ES6之前,JavaScript並沒有繼承這一現有的機制。

類式繼承

//宣告父類
function Father(){
    this.fatherVal = 'father';
}
//為父類新增共有方法
Father.prototype.getFatherValue = function(){
    return this.fatherVal;
}
//宣告子類 
function Child(){
    this.childVal = 'child';
}
//繼承父類
Child.prototype = new Father();
//為子類新增共有方法
Child.prototype.getChildValue = function(){
    return this.childVal;
}
複製程式碼

子類的prototype被賦予父類的例項,新建立的物件複製了父類的建構函式內的屬性和方法並且將原型_proto_指向了父類的原型物件,這樣就擁有了父類的原型物件上的屬性和方法與父類建構函式中複製的屬性和方法。

var instance = new Child();
console.log(instance.getFatherValue());  //father
console.log(instance.getChildValue());   //child
console.log(instance instanceof Child);  //true
console.log(instance instanceof Father); //true
console.log(instance instanceof Object); //true
console.log(Child instanceof Father);    //false
console.log(Child.prototype instanceof Father);    //true
複製程式碼

缺點:

  1. 子類例項共用父類的公有引用屬性。
  2. 無法對父類建構函式內的屬性進行傳參初始化。
function Father(){
    this.companies =['bigo','yy','uc']
}
funtion Child(){}
Child.prototype = new Father();
var instanceA = new Child();
var instanceB = new Child();
console.log(instanceB.companies); //['bigo','yy','uc']
instanceA.companies.push('nemo');
console.log(instanceB.companies); //['bigo','yy','uc','nemo']
複製程式碼

建構函式繼承

//宣告父類
function Father(val){
   this.companies =['bigo','yy','uc']
   this.val = val;
}
//宣告父類原型方法
Father.prototype.getCom = function(){
    console.log(this.companies);
}
//宣告子類
function Child(val){
    //繼承
    Father.call(this,val);
}
var instanceA = new Child('childA');
var instanceB = new Child('childB');

instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
console.log(instanceA.val); //childA
console.log(instanceB.companies); //['bigo','yy','uc']
console.log(instanceB.val); //childB
複製程式碼

對Child呼叫call,將子類中的變數在父類中執行一遍,然後父類給this繫結,所以子類繼承了父類的公有屬性。

缺點:

由於這種型別的繼承沒有涉及原型prototype,所以父類的原型方法不會被子類繼承,而如果想被子類繼承就必須放在建構函式中,這樣建立出來的每個例項都會單獨擁有一份而不能共用。

組合繼承

//宣告父類
function Father(val){
   this.companies =['bigo','yy','uc']
   this.val = val;
}
//宣告父類原型方法
Father.prototype.getValue = function(){
    console.log(this.val);
}
//宣告子類
function Child(val,newVal){
    //建構函式式繼承
    Father.call(this,val);
    this.newVal = newVal;
}
//類式繼承
Child.prototype = new Father();
//宣告子類原型方法
Child.prototype.getNewValue = function(){
    console.log(this.newVal);
}

var instanceA = new Child("fatherA","childA");
instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
instanceA.getValue(); //fatherA
instanceA.getNewValue(); //childA

var instanceB = new Child("fatherB","childB");
console.log(instanceA.companies); //['bigo','yy','uc']
instanceB.getValue(); //fatherB
instanceB.getNewValue(); //childB
複製程式碼

缺點:

在使用建構函式繼承使用執行了一遍父類的建構函式,在實現子類原型的類式繼承再呼叫了一遍父類的建構函式,父類建構函式被呼叫了兩次。

原型式繼承

function inheritObject(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var situation = {
    companies:['bigo','yy','uc'];
    area:'guangzhou';
}

var situationA = inheritObject(situation);
situationA.area = 'shenzhen';
situationA.companies.push('tencent');

var situationB = inheritObject(situation);
situationB.area = 'beijing';
situationB.companies.push('baidu');

console.log(situationA.area);   //shenzhen
console.log(situationA.companies); //['bigo','yy','uc','tencent','baidu']
console.log(situationB.area); //beijing
console.log(situationB.companies); //['bigo','yy','uc','tencent','baidu']
console.log(situation.area);  //guangzhou
console.log(situation.companies); //['bigo','yy','uc','tencent','baidu']
複製程式碼

是類式繼承的一個封裝,其中的過渡物件就相當於類式繼承的子類,然後返回新的例項化物件。

缺點:

跟類式繼承一樣,父類的公有引用屬性被共有。

寄生式繼承

function inheritObject(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

var situation = {
    companies:['bigo','yy','uc'];
    area:'guangzhou';
}

function createSituation(obj){
    //通過原型繼承建立新物件
    var newObj = new inheritObject(obj);
    //定義新物件方法
    newObj.getArea = function(){
        console.log(newObj.area)
    }
    //返回物件
    return obj;
}
複製程式碼

只是在原型式繼承的基礎上新增了新屬性和方法,還是跟原型式繼承一樣的缺點。

寄生式組合繼承

function inheritObject(obj){
    function F(){};
    F.prototype = obj;
    return new F();
}

//傳遞引數  child,parent 子類父類
function inheritPrototype(child,parent){
    //複製一份父類的原型副本儲存在變數中;
    var fatherProto = inheritObject(father.prototype);
    //修正因為重寫子類原型導致子類的constructor屬性被修改;
    fatherProto.constructor = child;
    //設定子類的原型
    child.prototype = fatherProto;
}


//宣告父類
function Father(val){
   this.companies =['bigo','yy','uc']
   this.val = val;
}
//宣告父類原型方法
Father.prototype.getValue = function(){
    console.log(this.val);
}
//宣告子類
function Child(val,newVal){
    //建構函式式繼承
    Father.call(this,val);
    this.newVal = newVal;
}
//類式繼承
Child.prototype = new Father();
inheritPrototype(Child,Father);
//宣告子類原型方法
Child.prototype.getNewValue = function(){
    console.log(this.newVal);
}
複製程式碼
  1. 在建構函式繼承中我們已經呼叫了父類的建構函式,還差一個原型的副本
  2. 通過原型繼承得到副本,但是這時候fatherProto的constructor需要指向子類。
  3. 最後將副本fatherProto賦給子類的原型prototype。

總的來說,就是既要建構函式,又要原型繼承,但是又避免了組合繼承的兩次呼叫父類建構函式的問題,最大的改變式對子類原型賦予的式父類原型的一個引用。

var instanceA = new Child("fatherA","childA");
instanceA.companies.push('nemo');
console.log(instanceA.companies); //['bigo','yy','uc','nemo']
instanceA.getValue(); //fatherA
instanceA.getNewValue(); //childA

var instanceB = new Child("fatherB","childB");
console.log(instanceA.companies); //['bigo','yy','uc']
instanceB.getValue(); //fatherB
instanceB.getNewValue(); //childB

複製程式碼

注意點:

此時子類如果需要新增原型方法,必須通過prototype點語法一個個新增,否則會覆蓋掉繼承父類的原型物件。

ES6的繼承方式

ES6 新增了Class語法,Class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。

Class 繼承

class Parent {
  constructor(value) {
    this.val = value
  }
  getValue() {
    console.log(this.val)
  }
}
class Child extends Parent {
  constructor(value) {
    super(value)
  }
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

複製程式碼

class 實現繼承的核心在於使用 extends 表明繼承自哪個父類,並且在子類建構函式中必須呼叫 super,因為這段程式碼可以看成 Parent.call(this, value)。

如果子類沒有定義constructor方法,這個方法會被預設新增。

相關文章