js實現繼承的幾種方式和對比

幽涯發表於2018-06-17

作為一門弱型別的程式語言,JS 是通過建構函式的方式實現物件導向程式設計的,下面就來探討一下 JS 的幾種繼承方式,並分析各種方式的優缺點;

父類

既然要實現繼承,那麼必須有一個父類,程式碼如下:

function Animal(name) {
    this.name = name;
}
Animal.prototype = {
    constructor: Animal,
    sleep: function() {
        console.log(this.name, 'is sleeping!');
    }
}
複製程式碼

由於方法是通用型的,個人覺得方法放到例項中是非常消耗效能的,畢竟建立建立一個例項,都會在記憶體空間中重新分配一部分空間用於儲存方法,所以此篇文章預設方法都是放到原型中;

繼承方式

1、原型鏈繼承

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

function Cat() {
    
}
Cat.prototype = new Animal('Cat');
Cat.prototype.constructor = Cat;    // 修復建構函式指向
var cat = new Cat();
console.log(cat.name);  // 'Cat'
cat.sleep();   // Cat is sleeping!
複製程式碼

優點:

  1. 純粹的繼承關係,子類原型複製了父類的例項屬性並加以擴充;
  2. 子類的原型繼承了父類的原型屬性,所以父類新新增的原型屬性都可以使用;
  3. 簡單易理解,結構清晰明瞭;

缺點:

  1. 難以實現多繼承(多個父類例項賦值給子類原型的屬性並不美觀);
  2. 父類的例項屬性值(並不是方法)存在於子類的原型中;對於建構函式來講,最理想的狀態就是屬性存在例項中,方法存在於原型中;
  3. 在定義子類的原型時就要建立父類的例項(傳參也在這一步完成);建立子類例項的時候父類的例項已經初始化完成,無法向父類傳參;如果建立子類的引數是非同步的就會造成困擾,除非子類例項定義一個與父類例項相同的引數進行覆蓋,但顯然這不是最好的方式,會造成子類例項和原型中出現相同的屬性;
  4. 想要為子類新增原型方法,必須在繼承了父類例項之後執行;

2、構造繼承

使用父類的構造器來增強子類的例項(將父類的例項賦予子類例項);

function Cat(name, age) {
    Animal.call(this, name);
    this.age = age || 0;
}
Cat.prototype = {
    constructor: Cat,
    eat: function() {
        console.log(this.name, 'is eating!');
    }
}
var cat = new Cat('Tom', 17)
console.log(cat.name);  // 'Tom'
cat.eat();  // 'Tom is eating!'
複製程式碼

優點:

  1. 初始化子類例項的時候,可以為父類傳遞引數;
  2. 父類的例項屬性會被初始化在子類例項中的,並不會在原型中;
  3. 方便實現多繼承;

缺點:

  1. 並沒有真正的繼承父類,只是複製了一份父類的例項屬性到子類中;
  2. 沒有繼承父類的原型方法,子類原型的再上一層繼承的仍然是 Object;

3、組合繼承

原型鏈繼承和構造繼承兩種方式的組合使用

function Cat(name) {
    Animal.call(this, name);
}
Cat.prototype = new Animal();   // 此處不用傳參
Cat.prototype.constructor = Cat;
var cat = new Cat('Tom');
console.log(cat.name);  // 'Tom'
cat.sleep();    // Tom is sleeping!
複製程式碼

優點:

  1. 彌補了「構造繼承」中不能繼承原型方法的問題;
  2. 彌補了「原型鏈繼承」中,在定義子類原型時就需要傳參的問題,以及難以實現多繼承問題;

缺點:

  1. 同樣存在「原型鏈繼承」中要為子類新增原型方法,需要在繼承了父類例項之後執行的問題;
  2. 一次繼承建立了兩份父類例項,一份複製在子類例項中,一份在子類原型中,使用時子類例項覆蓋了原型中的同名屬性,增大了記憶體消耗;

4、寄生組合繼承

通過寄生的方式,在繼承時砍掉父類的例項屬性,避免初始化兩次父類例項的問題;

if(typeof Function.prototype.extend === 'undefined') {
    // 寫法一
    Function.prototype.extend = function(Sup) {
        function O() {}
        O.prototype = Sup.prototype;
        this.prototype = new O();
        Object.defineProperty(this.prototype, 'constructor', {
            configurable: true,
            enumerable: false,
            writable: true,
            value: this
        });
    }
    // 寫法二
    Function.prototype.extend = function(Sup) {
        var Sub = this;
        Sub.prototype = Object.create(Sup.prototype, {
            constructor: {
                configurable: true,
                enumerable: false,
                writable: true,
                value: Sub
            }
        });
    }
}
function Cat(name) {
    Animal.call(this, name);
}
Cat.extend(Animal);
var cat = new Cat('Tom');
console.log(cat.name);
cat.sleep();
複製程式碼

優點:

  1. 非常乾淨的原型鏈,繼承方式趨向於完美;

缺點:

  1. 實現方式較為繁瑣和複雜;

以上就是我要介紹的 JS 實現繼承的幾種常用方式了,你們以為到這裡就已經結束了嗎?


ES6 出來已經有好長一段時間了,各瀏覽器對 ES6 語法和 API 的支援也越來越完善(就算還沒支援的,也有 babel 這類工具讓大家可提前使用 ES6 的各種新特性);

而 es6 本身的 calss 語法也實現了繼承的特性,有空的同學可以自己去看看;

下面補充一點

其實 js 本身並沒有繼承,而所謂的原型繼承可以稱為怪異繼承,但其本身並不是繼承,只是通過 new 呼叫函式,強行將生成的物件和函式的 prototype 物件進行關聯,可以實現對其方法的使用而已(詳細解釋請移步至《你不知道的 JavaScript 上卷》P146:5.2章 “類”)

相關文章