前端知識總結系列筆記四:js各類繼承方式以及優缺點

前言

與原型和原型鏈同樣,繼承也是面試中的常考點之一,工做中也用得比較多~,子類繼承自父類,能夠共享父類裏封裝好的方法。本文嘗試根據紅皮書總結並整理各類繼承方式的繼承思想以及優缺點分析,最終選出相對最優的一種繼承方案。javascript

1、原型鏈繼承

上文咱們介紹了原型和原型鏈的關係,知道了訪問一個對象的屬性和方法是沿着原型鏈一層一層向上查找的,那麼若是一個對象要繼承另一個對象原型上的屬性和方法,一個最簡單的方式就是讓這個對象的原型指向另一個對象。 所以,原型鏈繼承實現的本質就是重寫對象,代之以一個新類型的實例。java

function Father() {
    this.fatherName = 'papa';
}

Father.prototype.getFatherName = function() {
    return this.fatherName;
}

function Son() {
    this.sonName = 'baby';
}

// 建立Father的實例,賦值給Son的原型,這樣son就具備了Father構造函數上的屬性,也能訪問Father原型上的方法,這種就是原型鏈繼承。
Son.prototype = new Father();

Son.prototype.getSonName = function() {
    return this.sonName;
}

const son = new Son();
son.getFatherName(); // Output:papa
複製代碼

原型鏈繼承存在缺點:包含引用類型值的原型可能會被篡改es6

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

function SubType() {
}
// 繼承SuperType
SubType.prototype = new SuperType();

const instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black'];

const instance2 = new SubType();
console.log(instance1.colors); // ['red', 'blue', 'green', 'black'];
複製代碼

原型鏈繼承還存在另一個問題:沒有辦法在不影響全部對象實例的狀況下,向超類型的構造函數中傳遞參數。有鑑於此,加上剛纔所說的包含引用類型值的原型可能被篡改,實際開發中,不多會單獨使用原型鏈繼承。面試

2、借用構造函數

爲了解決原型鏈繼承帶來的問題,開發人員開始使用一種叫作借用構造函數(也叫僞造對象或經典繼承)的技術。基本思想是在子類型構造函數內部調用超類型構造函數,複製超類型的實例屬性給子類。bash

function SuperType() {
    this.colors = ['red', 'blue', 'green'];
}

function SubType() {
    // 繼承了SuperType
    SuperType.call(this); // 這裏是關鍵
}

const instance1 = new SubType();
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black'] 

const instance2 = new SubType();
console.log(instance2.colors); // ['red', 'blue', 'green']
複製代碼

核心代碼是SuperType.call(this),建立子類實例時調用SuperType構造函數,因而複製了SuperType的屬性colors給子類SubType,很好地解決了原型鏈繼承中包含引用類型值的原型可能會被篡改的缺點。 除此以外,借用構造函數繼承還解決了原型鏈繼承的另一個缺陷:沒法向超類型的構造函數中傳遞參數。在子類型構造函數內部運用call()方法(或apply())的時候就能夠給超類型的構造函數傳遞參數。app

若是單單使用借用構造函數實現繼承,也是會存在問題的:函數

  • 沒法實現函數複用,只能繼承父類的實例屬性和方法,不能繼承父類原型中定義的方法。
  • 子類的每一個實例都有父類實例函數的副本,影響性能。

3、組合繼承

以上兩種繼承方式都有各自的優缺點,原型鏈繼承能繼承父類原型上的屬性和方法,借用構造函數繼承能繼承父類的實例屬性和方法,組合以上的兩種繼承,即爲組合繼承。post

其核心思想就是使用原型鏈實現對原型屬性和方法的繼承,經過借用構造函數實現對實例屬性的繼承。性能

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function () {
    alert(this.name);
}

function SubType(name, age) {
    // 繼承屬性
    SuperType.call(this, name);
    
    this.age = age;
}

// 繼承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    alert(this.age);
}

const instance1 = new SubType('jiaxin', 16);
instance1.colors.push('black');
console.log(instance1.colors); // ['red', 'blue', 'green', 'black'];
instance1.sayName(); // jiaxin
instance1.sayAge(); // 16

const instance2 = new SubType('jingjing', 19);
console.log(instance2.colors); // ['red', 'blue', 'green']
instance2.sayName(); // 'jingjing'
instance2.sayAge(); // 19
複製代碼

優勢:ui

融合了原型鏈繼承和借用構造函數繼承的優勢,避免了他們的缺陷。

缺點: 在把SuperType父類的實例賦予給SubType子類的原型的時候,就在子類的原型鏈(proto)上繼承了父類的屬性,而子類的實例又在其內部繼承了父類的屬性和方法,至關於繼承了兩份相同的屬性和方法,一份存在原型中,另外一份存在子類實例的內部。

咱們把實例打印出來可更直觀的觀察(因爲屬性遮蔽,其原型上的存在的同名屬性/方法colors,name將不會被訪問到):

4、原型式繼承

核心思想:基於已有的對象建立一個新對象,該新對象的原型指向已有的對象,原理和es5的Object.create()大同小異。用new方式實現以下:

function object(obj) {
    function F() {};
    F.prototype = obj;
    return new F();
}
複製代碼

從本質上講,object()對傳入其中的對象執行了一次淺複製。

const person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"]
};

// 建立了一個新對象anotherPerson,其原型指向person
const anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

console.log(person.friends);   // ['Shelby', 'Court', 'Van', 'Rob', 'Barbie']
複製代碼

由上述代碼能夠看出,原型式繼承的缺點跟原型鏈繼承是同樣的:

  • 多個實例會共享原型上的屬性/方法,若是該屬性/方法包含引用類型,則存在被篡改的可能。
  • 沒法傳遞參數。

5、寄生式繼承

其核心思想是在原型式繼承的基礎上,以某種方式來加強對象,返回對象。

function createAnother(original) {
    const clone = object(original);
    clone.sayHi = function() {
        console.log('hi~');
    };
    return clone;
}
複製代碼

該函數的做用是給返回的對象新增屬性或方法,以加強函數。

const person = {
    name: 'Nicholas',
    friends: ['Shelby', 'Court', 'Van']
};

const anotherPerson = createAnother(person);
anotherPerson.sayHi(); // hi~
複製代碼

缺點(同原型式繼承同樣):

  • 原型上的屬性/方法存在篡改的可能;
  • 沒法傳遞參數,沒法作到函數複用而下降效率。

6、寄生組合式繼承

核心思想:寄生式繼承和組合繼承的結合,即經過借用構造函數繼承屬性,經過原型鏈的混合形式來繼承方法。

前面提到,組合式繼承最大的缺點就是會調用兩次超類型構造函數,在new一個子類實例時,copy了兩份同樣的數據,一份存在實例中,一份存在實例的原型上,所以原型中的同名屬性就會被屏蔽。而解決這個問題的方法就是寄生組合式繼承。

下面的inheritPrototype函數是實現寄生組合是繼承的最簡單形式。

function inheritPrototype(subType, superType) {
    const prototype = Object(superType.prototype); // 建立對象,建立超類型原型的一個副本
    prototype.constructor = subType; // 加強對象,彌補因重寫原型而失去的默認的constructor屬性
    subType.prototype = prototype; // 指定對象,將新建立的對象賦值給子類型的原型
}
複製代碼

使用:

function SuperType(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

SuperType.prototype.sayName = function() {
    console.log(this.name);
}

// 借用構造函數傳遞加強子類實例屬性
function SubType(name, age) {
    // 繼承父類的屬性/方法,此處指name、colors(支持傳參、避免篡改)
    // 只調用了一次SuperType構造函數
    SuperType.call(this, name);
    this.age = age;
}

// 繼承父類的原型上的方法/屬性,此處指sayName
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
    console.log(this.age);
}

const instance1 = new SubType('jiaxin', 16);
instance1.colors.push('pink');

const instance2 = new SubType('xiaohua', 30);
console.log(instance2.colors); // ['red', 'blue', 'green']
複製代碼

咱們把子類的實例(instance1)打印出來看看:

這種繼承方式的高效率體如今它只調用了一次SuperType構造函數,所以避免了再SubType.prototype上建立沒必要要的、多餘的屬性。與此同時,原型鏈還能保持比變,還可以正常使用instanceof和isPrototypeOf()。所以寄生組合繼承是引用類型最理想的繼承方式。

7、類Class繼承

ES6引入了Class的概念,Class繼承經過關鍵字extends實現。

class SuperType {
    // 定義父類的實例方法/屬性
    constructor(name, age) {
        this.name = name;
        this.age = age;
        this.colors = ['red', 'blue', 'green'];
    }
    // methods,定義父類原型上的方法
    sayHello() {
        console.log('hello~');
    }
}
class SubType extends SuperType {
    constructor(name, age, gender) {
        // super在這至關於SuperType.prototype.constructor.call(this, name,age)
        super(name, age);//super方法調用父類實例,只有調用super,纔可以使用this關鍵字
        this.gender = gender;
        super.sayHello();
    }
    
    // methods, 定義在子類原型上的方法
    sayAge() {
        console.log(this.age);
    }
}
const instance1 = new SubType('jiaxin', 16, 'female');
instance1.sayHello(); // hello~
instance1.sayAge(); // 16
instance1.colors.push('pink');
instance1.colors; // ['red', 'blue', 'green', 'pink'];

const instance2 = new SubType('xiaohua', 30, 'female');
instance2.colors; // ['red', 'blue', 'pink']
複製代碼

咱們把子類的實例(instance1)打印出來看看:

所以,咱們可看到,ES6類繼承其實跟ES5的寄生組合式繼承差很少,子類的多個實例共享父類的引用類型值時,可避免被篡改。

區別是ES5 的繼承,實質是先創造子類的實例對象this,而後再將父類的方法添加到this上面(Parent.apply(this))。ES6的繼承機制徹底不一樣,實質是先將父類實例對象的屬性和方法,加到this上面(因此必須先調用super方法),而後再用子類的構造函數修改this。

8、Mixin模式實現多個對象的繼承

Mixin 指的是多個對象合成一個新的對象,新對象具備各個組成成員的接口。 簡單實現就是使用Object.assign();

const a = {
  a: 'a'
};
const b = {
  b: 'b'
};
const c = Object.assign(a, b); // {a: 'a', b: 'b'}
複製代碼

下面是一個比較完備的實現:

function mix(...mixins) {
  class Mix {
    constructor() {
      for (let mixin of mixins) {
        copyProperties(this, new mixin()); // 拷貝實例屬性
      }
    }
  }

  for (let mixin of mixins) {
    copyProperties(Mix, mixin); // 拷貝靜態屬性
    copyProperties(Mix.prototype, mixin.prototype); // 拷貝原型屬性
  }

  return Mix;
}

function copyProperties(target, source) {
  for (let key of Reflect.ownKeys(source)) {
    if ( key !== 'constructor'
      && key !== 'prototype'
      && key !== 'name'
    ) {
      let desc = Object.getOwnPropertyDescriptor(source, key);
      Object.defineProperty(target, key, desc);
    }
  }
}
複製代碼

上面代碼的mix函數,能夠將多個對象合成爲一個類。使用的時候,只要繼承這個類便可。

class Child extends mix(Parent, GrandParent) {
  // TO DO
}
複製代碼

參考:

《javascript高級程序設計》6.3 繼承

阮一峯ECMAScript 6 入門 class的繼承

木易楊說 JavaScript經常使用八種繼承方案

相關文章
相關標籤/搜索