JS 系列三:繼承的 六 種實現方式

引言

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

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

ES5 繼承

先定義一個父類github

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

1、 原型鏈繼承

基本思想

將父類的實例做爲子類的原型設計模式

// 父類
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
複製代碼

原型鏈繼承

特色:

利用原型,讓一個引用類型繼承另外一個引用類型的屬性及方法架構

優勢:

繼承了父類的模板,又繼承了父類的原型對象app

缺點:

  • 能夠在子類構造函數中,爲子類實例增長實例屬性。若是要新增原型屬性和方法,則必須放在 SubType.prototype = new SuperType('SubType'); 這樣的語句以後執行。異步

  • 沒法實現多繼承ide

  • 來自原型對象的全部屬性被全部實例共享函數

    // 父類
    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 全部實例共享這一更新。基礎類型屬性更新則不會。post

  • 建立子類實例時,沒法向父類構造函數傳參,或者說是,沒辦法在不影響全部對象實例的狀況下,向超類的構造函數傳遞參數

2、 構造繼承

基本思想:

在子類型的構造函數內部調用父類型構造函數。

注意:

  • 函數只不過是在特定環境中執行代碼的對象,因此這裏使用 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. 👇歡迎關注:前端瓶子君,每日更新!👇

前端瓶子君
相關文章
相關標籤/搜索