引言
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
注意:更改
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
第一次呼叫 SuperType
建構函式時,SubType.prototype
會得到兩個屬性name
和colors
;當呼叫 SubType
建構函式時,第二次呼叫 SuperType
建構函式,這一次又在新物件屬性上建立了 name
和colors
,這兩個屬性就會遮蔽原型物件上的同名屬性。
// 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
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片儲存下來直接上傳(img-72jtuatW-1677146968659)(https://p1-jj.byteimg.com/tos...)]
優點:
彌補了方式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
建構函式,只建立一份父類屬性 - 原型鏈保持不變
- 能夠正常使用
instanceof
與isPrototypeOf
五. 原型式繼承
實現思路:
實現思路就是將子類的原型設定為父類的原型
// 父類
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')
多繼承
如果希望能 多繼承 ,可使用 混入 的方式
// 父類 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的工作方式,由於原型鏈等特性的存在,在不同物件之間功能的共享通常被叫做 委託 - 特殊的物件將功能委託給通用的物件型別完成。這也許比將其稱之為繼承更為貼切,因為“被繼承”了的功能並沒有被複製到正在“進行繼承”的物件中,相反它仍存在於通用的物件中。