JS 繼承的 六 種實現方式

前端瓶子君發表於2019-09-02

引言

JS系列暫定 27 篇,從基礎,到原型,到非同步,到設計模式,到架構模式等,

本篇是 JS系列中第 3 篇,文章主講 JS 繼承,包括原型鏈繼承、建構函式繼承、組合繼承、寄生組合繼承、原型式繼承、 ES6 繼承,以及 多繼承與 new 。

ES5 繼承

先定義一個父類

function SuperType () {
  // 屬性
  this.name = 'SuperType';
}
// 原型方法
SuperType.prototype.sayName = function() {
  return this.name;
};
複製程式碼

一、 原型鏈繼承

基本思想

將父類的例項作為子類的原型

// 父類
function SuperType () {
  this.name = 'SuperType'; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
  return this.name;
};

// 子類
function SubType () {
  this.subName = "SubType"; // 子類屬性
};

SubType.prototype = new SuperType(); // 重寫原型物件,代之以一個新型別的例項
// 這裡例項化一個 SuperType 時, 實際上執行了兩步
// 1,新建立的物件複製了父類建構函式內的所有屬性及方法
// 2,並將原型 __proto__ 指向了父類的原型物件

SubType.prototype.saySubName = function () { // 子類原型方法
  return this.subName;
}

// 子類例項
let instance = new SubType();

// instanceof 通過判斷物件的 prototype 鏈來確定物件是否是某個類的例項
instance instanceof SubType; // true
instance instanceof SuperType; // true

// 注意
SubType instanceof SuperType; // false
SubType.prototype instanceof SuperType ; // true
複製程式碼

原型鏈繼承

特點:

利用原型,讓一個引用型別繼承另一個引用型別的屬性及方法

優點:

繼承了父類的模板,又繼承了父類的原型物件

缺點:

  • 可以在子類建構函式中,為子類例項增加例項屬性。如果要新增原型屬性和方法,則必須放在 SubType.prototype = new SuperType('SubType'); 這樣的語句之後執行。

  • 無法實現多繼承

  • 來自原型物件的所有屬性被所有例項共享

    // 父類
    function SuperType () {
      this.colors = ["red", "blue", "green"];
      this.name = "SuperType";
    }
    // 子類
    function SubType () {}
    
    // 原型鏈繼承
    SubType.prototype = new SuperType();
    
    // 例項1
    var instance1 = new SubType();
    instance1.colors.push("blcak");
    instance1.name = "change-super-type-name";
    console.log(instance1.colors); // ["red", "blue", "green", "blcak"]
    console.log(instance1.name); // change-super-type-name
    // 例項2
    var instance2 = new SubType();
    console.log(instance2.colors); // ["red", "blue", "green", "blcak"]
    console.log(instance2.name); // SuperType
    複製程式碼

    prototype-shared

    注意:更改 SuperType 引用型別屬性時,會使 SubType 所有例項共享這一更新。基礎型別屬性更新則不會。

  • 建立子類例項時,無法向父類建構函式傳參,或者說是,沒辦法在不影響所有物件例項的情況下,向超類的建構函式傳遞引數

二、 構造繼承

基本思想:

在子型別的建構函式內部呼叫父型別建構函式。

注意:

  • 函式只不過是在特定環境中執行程式碼的物件,所以這裡使用 apply/call 來實現。

  • 使用父類的建構函式來增強子類例項,等於是複製父類的例項屬性給子類(沒用到原型)

// 父類
function SuperType (name) {
  this.name = name; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
  return this.name;
};

// 子類
function SubType () {
  // 呼叫 SuperType 建構函式
  SuperType.call(this, 'SuperType'); // 在子類建構函式中,向父類建構函式傳參
  // 為了保證子父類的建構函式不會重寫子類的屬性,需要在呼叫父類建構函式後,定義子類的屬性
  this.subName = "SubType"; // 子類屬性
};
// 子類例項
let instance = new SubType(); // 執行子類建構函式,並在子類建構函式中執行父類建構函式,this繫結到子類
複製程式碼

建構函式繼承

優點:

解決了1中子類例項共享父類引用物件的問題,實現多繼承,建立子類例項時,可以向父類傳遞引數

缺點:

  • 例項並不是父類的例項,只是子類的例項
  • 只能繼承父類的例項屬性和方法,不能繼承原型屬性/方法
  • 無法實現函式複用,每個子類都有父類例項函式的副本,影響效能

三. 組合繼承

顧名思義,組合繼承就是將原型鏈繼承與建構函式繼承組合在一起,從而發揮兩者之長的一種繼承模式。

基本思想:

使用原型鏈繼承使用對原型屬性和方法的繼承,通過建構函式繼承來實現對例項屬性的繼承。這樣既能通過在原型上定義方法實現函式複用,又能保證每個例項都有自己的屬性。

通過呼叫父類構造,繼承父類的屬性並保留傳參的優點,然後通過將父類例項作為子類原型,實現函式複用

// 父類
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
  return this.name;
};

// 子類
function SubType (name, subName) {
  // 呼叫 SuperType 建構函式
  SuperType.call(this, name); // ----第二次呼叫 SuperType----
  this.subName = subName;
};

// ----第一次呼叫 SuperType----
SubType.prototype = new SuperType(); // 重寫原型物件,代之以一個新型別的例項

SubType.prototype.constructor = SubType; // 組合繼承需要修復建構函式指向
SubType.prototype.saySubName = function () { // 子類原型方法
  return this.subName;
}

// 子類例項
let instance = new SubType('An', 'sisterAn')
instance.colors.push('black')
console.log(instance.colors) // ["red", "blue", "green", "black"]
instance.sayName() // An
instance.saySubName() // sisterAn

let instance1 = new SubType('An1', 'sisterAn1')
console.log(instance1.colors) //  ["red", "blue", "green"]
instance1.sayName() // An1
instance1.saySubName() // sisterAn1
複製程式碼

組合繼承1

第一次呼叫 SuperType 建構函式時,SubType.prototype 會得到兩個屬性namecolors;當呼叫 SubType 建構函式時,第二次呼叫 SuperType 建構函式,這一次又在新物件屬性上建立了 namecolors,這兩個屬性就會遮蔽原型物件上的同名屬性。

// instanceof:instance 的原型鏈是針對 SuperType.prototype 進行檢查的
instance instanceof SuperType // true
instance instanceof SubType // true

// isPrototypeOf:instance 的原型鏈是針對 SuperType 本身進行檢查的
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
複製程式碼

組合繼承2

優點:

彌補了方式2的缺陷,可以繼承例項屬性/方法,也可以繼承原型屬性/方法,不存在引用屬性共享問題,可傳參,可複用

缺點:

  • 呼叫了兩次父類建構函式,生成了兩份例項(子類例項將子類原型上的那份遮蔽了)

四. 寄生組合繼承

在組合繼承中,呼叫了兩次父類建構函式,這裡 通過通過寄生方式,砍掉父類的例項屬性,這樣,在呼叫兩次父類的構造的時候,就不會初始化兩次例項方法/屬性,避免的組合繼承的缺點

主要思想:

借用 建構函式 繼承 屬性 ,通過 原型鏈的混成形式 來繼承 方法

// 父類
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
  return this.name;
};

// 子類
function SubType (name, subName) {
  // 呼叫 SuperType 建構函式
  SuperType.call(this, name); // ----第二次呼叫 SuperType,繼承例項屬性----
  this.subName = subName;
};

// ----第一次呼叫 SuperType,繼承原型屬性----
SubType.prototype = Object.create(SuperType.prototype)

SubType.prototype.constructor = SubType; // 注意:增強物件

let instance = new SubType('An', 'sisterAn')
複製程式碼

寄生組合

優點:

  • 只呼叫一次 SuperType 建構函式,只建立一份父類屬性
  • 原型鏈保持不變
  • 能夠正常使用 instanceofisPrototypeOf

五. 原型式繼承

實現思路:

實現思路就是將子類的原型設定為父類的原型

// 父類
function SuperType (name) {
  this.colors = ["red", "blue", "green"];
  this.name = name; // 父類屬性
}
SuperType.prototype.sayName = function () { // 父類原型方法
  return this.name;
};

/** 第一步 */
// 子類,通過 call 繼承父類的例項屬性和方法,不能繼承原型屬性/方法
function SubType (name, subName) {
  SuperType.call(this, name); // 呼叫 SuperType 的建構函式,並向其傳參 
  this.subName = subName;
}

/** 第二步 */
// 解決 call 無法繼承父類原型屬性/方法的問題
// Object.create 方法接受傳入一個作為新建立物件的原型的物件,建立一個擁有指定原型和若干個指定屬性的物件
// 通過這種方法指定的任何屬性都會覆蓋原型物件上的同名屬性
SubType.prototype = Object.create(SuperType.prototype, { 
  constructor: { // 注意指定 SubType.prototype.constructor = SubType
    value: SubType,
    enumerable: false,
    writable: true,
    configurable: true
  },
  run : { 
    value: function(){ // override
      SuperType.prototype.run.apply(this, arguments); 
      	// call super
      	// ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
}) 

/** 第三步 */
// 最後:解決 SubType.prototype.constructor === SuperType 的問題
// 這裡,在上一步已經指定,這裡不需要再操作
// SubType.prototype.constructor = SubType;

var instance = new SubType('An', 'sistenAn')
複製程式碼

原型繼承1

多繼承

如果希望能 多繼承 ,可使用 混入 的方式

// 父類 SuperType
function SuperType () {}
// 父類 OtherSuperType
function OtherSuperType () {}

// 多繼承子類
function AnotherType () {
    SuperType.call(this) // 繼承 SuperType 的例項屬性和方法
    OtherSuperType.call(this) // 繼承 OtherSuperType 的例項屬性和方法
}

// 繼承一個類
AnotherType.prototype = Object.create(SuperType.prototype);

// 使用 Object.assign 混合其它
Object.assign(AnotherType.prototype, OtherSuperType.prototype);
// Object.assign 會把  OtherSuperType 原型上的函式拷貝到 AnotherType 原型上,使 AnotherType 的所有例項都可用 OtherSuperType 的方法

// 重新指定 constructor
AnotherType.prototype.constructor = AnotherType;

AnotherType.prototype.myMethod = function() {
     // do a thing
};

let instance = new AnotherType()
複製程式碼

最重要的部分是:

  • SuperType.call 繼承例項屬性方法
  • Object.create() 來繼承原型屬性與方法
  • 修改 SubType.prototype.constructor的指向

ES6 繼承

首先,實現一個簡單的 ES6 繼承:

class People {
    constructor(name) {
        this.name = name
    }
    run() { }
}

// extends 相當於方法的繼承
// 替換了上面的3行程式碼
class Man extends People {
    constructor(name) {
        // super 相當於屬性的繼承
        // 替換了 People.call(this, name)
        super(name)
        this.gender = '男'
    }
    fight() { }
}
複製程式碼

核心程式碼

extends 繼承的核心程式碼如下,其實現和上述的寄生組合式繼承方式一樣

function _inherits(subType, superType) {
    // 建立物件,Object.create 建立父類原型的一個副本
    // 增強物件,彌補因重寫原型而失去的預設的 constructor 屬性
    // 指定物件,將新建立的物件賦值給子類的原型 subType.prototype
    subType.prototype = Object.create(superType && superType.prototype, {
        constructor: { // 重寫 constructor
            value: subType,
            enumerable: false,
            writable: true,
            configurable: true
        }
    });
    if (superType) {
        Object.setPrototypeOf 
            ? Object.setPrototypeOf(subType, superType) 
            : subType.__proto__ = superType;
    }
}
複製程式碼

繼承的使用場景

  • 不要僅僅為了使用而使用它們,這只是在浪費時間而已。
  • 當需要建立 一系列擁有相似特性的物件 時,那麼建立一個包含所有共有功能的通用物件,然後在更特殊的物件型別中繼承這些特性。
  • 應避免多繼承,造成混亂。

注: 考慮到JavaScript的工作方式,由於原型鏈等特性的存在,在不同物件之間功能的共享通常被叫做 委託 - 特殊的物件將功能委託給通用的物件型別完成。這也許比將其稱之為繼承更為貼切,因為“被繼承”了的功能並沒有被拷貝到正在“進行繼承”的物件中,相反它仍存在於通用的物件中。

系列文章

想看更過系列文章,點選前往 github 部落格主頁

走在最後

1. ❤️玩得開心,不斷學習,並始終保持編碼。??

2. 如有任何問題或更獨特的見解,歡迎評論或直接聯絡瓶子君(公眾號回覆 123 即可)!??

3. ?歡迎關注:前端瓶子君,每日更新!?

前端瓶子君

相關文章