作為一門弱型別的程式語言,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!
複製程式碼
優點:
- 純粹的繼承關係,子類原型複製了父類的例項屬性並加以擴充;
- 子類的原型繼承了父類的原型屬性,所以父類新新增的原型屬性都可以使用;
- 簡單易理解,結構清晰明瞭;
缺點:
- 難以實現多繼承(多個父類例項賦值給子類原型的屬性並不美觀);
- 父類的例項屬性值(並不是方法)存在於子類的原型中;對於建構函式來講,最理想的狀態就是屬性存在例項中,方法存在於原型中;
- 在定義子類的原型時就要建立父類的例項(傳參也在這一步完成);建立子類例項的時候父類的例項已經初始化完成,無法向父類傳參;如果建立子類的引數是非同步的就會造成困擾,除非子類例項定義一個與父類例項相同的引數進行覆蓋,但顯然這不是最好的方式,會造成子類例項和原型中出現相同的屬性;
- 想要為子類新增原型方法,必須在繼承了父類例項之後執行;
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!'
複製程式碼
優點:
- 初始化子類例項的時候,可以為父類傳遞引數;
- 父類的例項屬性會被初始化在子類例項中的,並不會在原型中;
- 方便實現多繼承;
缺點:
- 並沒有真正的繼承父類,只是複製了一份父類的例項屬性到子類中;
- 沒有繼承父類的原型方法,子類原型的再上一層繼承的仍然是 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!
複製程式碼
優點:
- 彌補了「構造繼承」中不能繼承原型方法的問題;
- 彌補了「原型鏈繼承」中,在定義子類原型時就需要傳參的問題,以及難以實現多繼承問題;
缺點:
- 同樣存在「原型鏈繼承」中要為子類新增原型方法,需要在繼承了父類例項之後執行的問題;
- 一次繼承建立了兩份父類例項,一份複製在子類例項中,一份在子類原型中,使用時子類例項覆蓋了原型中的同名屬性,增大了記憶體消耗;
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();
複製程式碼
優點:
- 非常乾淨的原型鏈,繼承方式趨向於完美;
缺點:
- 實現方式較為繁瑣和複雜;
以上就是我要介紹的 JS 實現繼承的幾種常用方式了,你們以為到這裡就已經結束了嗎?
ES6 出來已經有好長一段時間了,各瀏覽器對 ES6 語法和 API 的支援也越來越完善(就算還沒支援的,也有 babel 這類工具讓大家可提前使用 ES6 的各種新特性);
而 es6 本身的 calss 語法也實現了繼承的特性,有空的同學可以自己去看看;
下面補充一點
其實 js 本身並沒有繼承,而所謂的原型繼承可以稱為怪異繼承,但其本身並不是繼承,只是通過 new 呼叫函式,強行將生成的物件和函式的 prototype 物件進行關聯,可以實現對其方法的使用而已(詳細解釋請移步至《你不知道的 JavaScript 上卷》P146:5.2章 “類”)