JavaScript六種繼承方式總結(jié)大全
1. 原型鏈繼承
是什么?
這是最基礎的繼承方式。其核心是:讓一個構(gòu)造函數(shù)的 prototype 對象指向另一個構(gòu)造函數(shù)的實例。這樣,子類就能通過原型鏈訪問到父類的屬性和方法。
javascript
// 父類
function Parent() {
this.name = 'Parent';
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子類
function Child() {
this.type = 'Child';
}
// 關(guān)鍵!將Child的原型指向Parent的實例,建立原型鏈
Child.prototype = new Parent();
var child1 = new Child();
child1.sayName(); // 'Parent' (來自原型鏈)
console.log(child1.colors); // ['red', 'blue'] (來自原型鏈)
var child2 = new Child();
child1.colors.push('green'); // 修改child1的colors
console.log(child2.colors); // ['red', 'blue', 'green'] (問題出現(xiàn)了!)有什么作用?
實現(xiàn)屬性和方法的繼承。
實際開發(fā)運用場景?
在現(xiàn)代前端開發(fā)中,幾乎不會單獨使用原型鏈繼承,因為它有致命缺陷。但它是一切其他繼承方式的理論基礎。
優(yōu)點:
簡單易懂,是理解JS繼承的基礎。
能夠繼承父類原型上的方法。
缺點:
引用類型屬性被所有實例共享(如上例中的
colors數(shù)組)。一個實例修改了引用類型屬性,所有實例都會受到影響。這是最大的問題。創(chuàng)建子類實例時,無法向父類構(gòu)造函數(shù)傳參(因為
new Parent()在初始化原型時就執(zhí)行了)。
2. 借用構(gòu)造函數(shù)繼承(經(jīng)典繼承)
是什么?
為了解決原型鏈繼承的缺點,這種方法的核心是:在子類構(gòu)造函數(shù)的內(nèi)部調(diào)用父類構(gòu)造函數(shù)。這利用了 call() 或 apply() 方法,使父類的 this 指向子類的實例。
javascript
// 父類
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
// 注意:父類原型上的方法子類訪問不到
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子類
function Child(name, type) {
// 關(guān)鍵!“借用”父類的構(gòu)造函數(shù)來初始化屬性
Parent.call(this, name); // 相當于執(zhí)行了 this.name = name; this.colors = ['red', 'blue'];
this.type = type;
}
var child1 = new Child('小明', 'Child');
var child2 = new Child('小紅', 'Child');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
console.log(child2.colors); // ['red', 'blue'] (互不影響了!)
console.log(child1.sayName); // undefined (無法繼承父類原型的方法)有什么作用?
解決原型鏈繼承中“引用屬性共享”和“無法傳參”的問題。
實際開發(fā)運用場景?
通常不會單獨使用,但它是組合繼承的重要組成部分。
優(yōu)點:
避免了引用屬性共享的問題。
可以在子類中向父類傳遞參數(shù)。
缺點:
方法都在構(gòu)造函數(shù)中定義,每次創(chuàng)建實例都會創(chuàng)建一遍方法,無法實現(xiàn)函數(shù)復用,效率低。
無法繼承父類原型(prototype)上的方法(如上例中的
sayName)。
3. 組合繼承
是什么?
組合繼承結(jié)合了原型鏈繼承和借用構(gòu)造函數(shù)繼承的優(yōu)點,是 JavaScript 中最常用的繼承模式。其核心是:
使用借用構(gòu)造函數(shù)來繼承屬性(解決共享和傳參問題)。
使用原型鏈來繼承方法(實現(xiàn)方法復用)。
javascript
// 父類
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子類
function Child(name, type) {
// 1. 繼承屬性
Parent.call(this, name); // 第二次調(diào)用 Parent()
this.type = type;
}
// 2. 繼承方法
Child.prototype = new Parent(); // 第一次調(diào)用 Parent()
// 修復構(gòu)造函數(shù)指向,否則Child實例的constructor會指向Parent
Child.prototype.constructor = Child;
// 子類自己的方法
Child.prototype.sayType = function() {
console.log(this.type);
};
var child1 = new Child('小明', 'Child1');
child1.colors.push('green');
console.log(child1.colors); // ['red', 'blue', 'green']
child1.sayName(); // '小明'
child1.sayType(); // 'Child1'
var child2 = new Child('小紅', 'Child2');
console.log(child2.colors); // ['red', 'blue'] (屬性不共享)
child2.sayName(); // '小紅' (方法可復用)有什么作用?
綜合了兩種模式的優(yōu)點,成為 JavaScript 中一種非常實用的繼承模式。
實際開發(fā)運用場景?
在 ES6 的 class 出現(xiàn)之前,這是最主流、最可靠的繼承方式。常用于構(gòu)建復雜的對象系統(tǒng),如UI組件庫、游戲引擎中的實體繼承等。
優(yōu)點:
實例擁有獨立的屬性,不會共享。
實例可以復用父類原型上的方法。
可以向父類構(gòu)造函數(shù)傳參。
缺點:
最大的缺點:會兩次調(diào)用父類構(gòu)造函數(shù)。
一次在
Parent.call(this)。一次在
new Parent()。
這導致子類實例和原型上存在兩份相同的屬性(一份在實例自身,一份在__proto__里),造成了一些不必要的浪費(雖然實例自身的屬性會屏蔽原型上的屬性,沒有功能問題)。
4. 原型式繼承
是什么?
道格拉斯·克羅克福德提出的方法。其核心是:創(chuàng)建一個臨時的構(gòu)造函數(shù),將其原型指向某個對象,然后返回這個臨時構(gòu)造函數(shù)的實例。本質(zhì)上是對傳入的對象進行了一次淺復制。
javascript
// object() 就是 ES5 中 Object.create() 的模擬實現(xiàn)
function object(o) {
function F() {} // 創(chuàng)建一個臨時構(gòu)造函數(shù)
F.prototype = o; // 將其原型指向傳入的對象o
return new F(); // 返回這個臨時構(gòu)造函數(shù)的實例
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court']
};
var person1 = object(person);
person1.name = 'Greg';
person1.friends.push('Rob');
var person2 = object(person);
person2.name = 'Linda';
person2.friends.push('Barbie');
console.log(person.friends); // ['Shelby', 'Court', 'Rob', 'Barbie'] (共享問題依然存在)有什么作用?
在不必興師動眾地創(chuàng)建構(gòu)造函數(shù)的情況下,基于一個已有對象創(chuàng)建新對象。
實際開發(fā)運用場景?
適用于簡單對象的淺拷貝繼承。
ES5 的
Object.create()方法規(guī)范化了原型式繼承?,F(xiàn)在直接使用Object.create()即可。
優(yōu)點:
無需創(chuàng)建構(gòu)造函數(shù),代碼簡潔。
缺點:
和原型鏈繼承一樣,存在引用屬性共享的問題。
5. 寄生式繼承
是什么?
寄生式繼承的思路與寄生構(gòu)造函數(shù)和工廠模式類似。其核心是:創(chuàng)建一個僅用于封裝繼承過程的函數(shù),該函數(shù)在內(nèi)部以某種方式(如原型式繼承)增強對象,最后返回這個對象。
javascript
function createAnother(original) {
var clone = object(original); // 1. 通過調(diào)用函數(shù)(如object)創(chuàng)建一個新對象(原型式繼承)
clone.sayHi = function() { // 2. 以某種方式來增強這個對象
console.log('hi');
};
return clone; // 3. 返回這個對象
}
var person = {
name: 'Nicholas',
friends: ['Shelby', 'Court']
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); // 'hi'有什么作用?
主要關(guān)注對象而不是自定義類型和構(gòu)造函數(shù),在主要考慮對象而非自定義類型和構(gòu)造函數(shù)的情況下,實現(xiàn)簡單的繼承和擴展。
實際開發(fā)運用場景?
適用于為對象添加一些額外功能的場景,但不廣泛使用。
優(yōu)點:
可以在不創(chuàng)建構(gòu)造函數(shù)的情況下,為對象添加函數(shù)。
缺點:
函數(shù)難以復用,效率低(跟借用構(gòu)造函數(shù)模式一樣)。
存在引用屬性共享的問題(跟原型式繼承一樣)。
6. 寄生組合式繼承
是什么?
這是組合繼承的優(yōu)化版本,也是目前公認的最理想的繼承范式。它解決了組合繼承調(diào)用兩次父類構(gòu)造函數(shù)的問題。
其核心是:
使用借用構(gòu)造函數(shù)來繼承屬性。
使用寄生式繼承來繼承父類原型,并將其賦值給子類原型。
javascript
function inheritPrototype(child, parent) {
// 1. 創(chuàng)建父類原型的一個副本(原型式繼承)
var prototype = Object.create(parent.prototype);
// 2. 修復副本的constructor指針
prototype.constructor = child;
// 3. 將子類的原型指向這個副本
child.prototype = prototype;
}
// 父類
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
// 子類
function Child(name, type) {
// 只調(diào)用一次父類構(gòu)造函數(shù)(繼承屬性)
Parent.call(this, name);
this.type = type;
}
// 關(guān)鍵!替換掉組合繼承中的 `Child.prototype = new Parent()`
inheritPrototype(Child, Parent);
// 添加子類自己的方法
Child.prototype.sayType = function() {
console.log(this.type);
};
var child1 = new Child('小明', 'Child1');
// 實例的 __proto__ 指向 Child.prototype
// Child.prototype 的 __proto__ 指向 Parent.prototype
// 完美!有什么作用?
只調(diào)用一次父類構(gòu)造函數(shù),并且避免了在子類原型上創(chuàng)建不必要的、多余的屬性。同時,原型鏈還能保持不變。
寄生組合式繼承核心目標:修復“組合繼承”的缺陷
要理解“寄生組合”,我們必須先回顧“組合繼承”的問題。
組合繼承的做法:
Parent.call(this):在子類構(gòu)造函數(shù)里調(diào)用父類構(gòu)造函數(shù)。這會在子類實例自身上創(chuàng)建一份父類的屬性。Child.prototype = new Parent():將子類的原型指向一個父類的實例。這會在子類的原型對象上創(chuàng)建第二份父類的屬性。
這就導致了:
子類實例上有
name和colors屬性。子類實例的
__proto__(也就是Child.prototype)上也有name和colors屬性。
雖然實例自身的屬性會屏蔽掉原型上的同名屬性,功能上沒問題,但多創(chuàng)建了一份多余的屬性,造成了內(nèi)存浪費和不優(yōu)雅。
寄生組合式繼承的解決方案
它的核心思路非常巧妙:我們真的需要那個 new Parent() 實例來充當原型嗎?不,我們只需要父類原型上的方法。
我們不需要 Parent 實例上的屬性(因為我們已經(jīng)通過 Parent.call(this) 在子類實例上得到了一份),我們只需要能通過原型鏈找到 Parent.prototype 上的方法。
所以,新的方案是:
繼承屬性:保持不變,依然在子類構(gòu)造函數(shù)里用
Parent.call(this)在子類構(gòu)造函數(shù)里調(diào)用父類構(gòu)造函數(shù),這會在子類實例自身上創(chuàng)建一份父類的屬性。這保證了每個實例都有自己獨立的屬性。繼承方法:不再用
new Parent(),而是直接創(chuàng)建一個純凈的、指向父類原型的對象,用它來作為子類的原型。
這個“純凈的、指向父類原型的對象”就是 Object.create(Parent.prototype) 所做的事情。
一步步拆解(超級詳細版)
讓我們把 inheritPrototype(Child, Parent) 這個函數(shù)里的三步拆開來看:
javascript
function inheritPrototype(child, parent) {
// 第一步:創(chuàng)建原型副本(核心)
var prototype = Object.create(parent.prototype);
// 第二步:修復constructor指向
prototype.constructor = child;
// 第三步:將子類的原型指向這個新創(chuàng)建的對象
child.prototype = prototype;
}第一步:var prototype = Object.create(parent.prototype);
Object.create()方法會創(chuàng)建一個新對象。這個新對象的
__proto__會指向你傳入的參數(shù),也就是parent.prototype。想象一下:這就好比我們憑空造了一個空對象
{},并且讓這個空對象“認”Parent.prototype為它的爸爸(原型)。這個空對象自己沒有任何屬性(解決了屬性重復的問題),但它可以順著原型鏈找到 Parent.prototype 上的所有方法(sayName)。
第二步:prototype.constructor = child;
任何一個
prototype對象都有一個constructor屬性,默認指向它關(guān)聯(lián)的構(gòu)造函數(shù)。因為我們用
Object.create()創(chuàng)建的新對象,它的constructor指向的是parent(因為它繼承自parent.prototype,而parent.prototype.constructor指向parent)。這顯然不對,我們希望子類原型的 constructor 指向子類自己 child。
所以我們需要手動糾正一下,讓
prototype.constructor = child;。
第三步:child.prototype = prototype;
最后,我們把這個我們精心制作好的、純凈的、鏈接到了父類原型的、constructor指正確的
prototype對象,賦值給子類的prototype。從此,所有
new Child()出來的實例,它們的__proto__都指向我們這個prototype對象,從而可以順利地通過原型鏈調(diào)用父類的方法。
終極比喻:“繼承家產(chǎn)”的故事
父類 (
Parent):一個富豪老爹,他有金庫(實例屬性name,colors)和一本生意經(jīng)(原型方法sayName)。組合繼承:老爹先給你復制了一本完整的生意經(jīng)(包括金庫的地圖),然后你又自己去金庫里拿了一次金子。你手里有金子,書上也有金子的地圖,地圖是多余的。
寄生組合繼承:一個聰明的律師(
Object.create)出現(xiàn)了。他沒有復制整本生意經(jīng),而是只做了一張神奇的索引卡。這張索引卡本身是空白的(沒有多余的屬性),但它直接指向了老爹那本生意經(jīng)的原始內(nèi)容(Parent.prototype)。然后你又自己去金庫里拿了一次金子。你手里有金子,也通過索引卡學會了老爹的生意經(jīng),完美!
總結(jié):為什么它是最佳的?
| 特性 | 組合繼承 | 寄生組合繼承 | 優(yōu)勢 |
|---|---|---|---|
| 父類屬性副本 | 2份 (實例上1份,原型上1份) | 1份 (僅在實例上) | 更高效,無浪費 |
| 父類方法繼承 | 通過原型鏈繼承 | 通過原型鏈繼承 | 同樣有效 |
| 父類構(gòu)造函數(shù)調(diào)用 | 2次 | 1次 | 性能更優(yōu) |
| 原型鏈 | 保持正確 | 保持正確 | 同樣正確 |
所以,寄生組合式繼承的核心貢獻就是:
它用一種極其巧妙的方式(Object.create)建立了子類和父類原型的直接聯(lián)系,完全跳過了創(chuàng)建父類實例這個不必要的步驟,從而避免了創(chuàng)建多余的屬性,完美地實現(xiàn)了繼承。這也是為什么ES6的 class 和 extends 語法糖其底層實現(xiàn)原理是寄生組合繼承的原因,因為它確實是理論上最完美的方案
實際開發(fā)運用場景?
這是實現(xiàn)基于構(gòu)造函數(shù)的繼承的最佳模式。在需要高度優(yōu)化和避免不必要的內(nèi)存開銷的庫或框架中可能會看到。但在日常開發(fā)中,我們更傾向于使用 ES6 的 class 和 extends,其底層原理就是寄生組合式繼承。
優(yōu)點:
只調(diào)用一次父類構(gòu)造函數(shù),效率高。
避免了在子類原型上創(chuàng)建不必要的屬性。
原型鏈保持不變,能正常使用
instanceof和isPrototypeOf。
缺點:
實現(xiàn)起來相對復雜。但通??梢苑庋b成一個函數(shù)(如上面的
inheritPrototype)來復用。
總結(jié)與建議
| 繼承方式 | 核心思想 | 優(yōu)點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| 原型鏈繼承 | 子類原型 = 父類實例 | 簡單,方法可復用 | 引用共享,無法傳參 | 基礎學習,幾乎不用 |
| 借用構(gòu)造函數(shù) | 在子類中 Parent.call(this) | 屬性獨立,可傳參 | 方法不能復用 | 組合繼承的一部分 |
| 組合繼承 | 借用構(gòu)造 + 原型鏈 | 屬性獨立,方法可復用,可傳參 | 調(diào)用兩次父類構(gòu)造函數(shù) | ES6前的主流方式 |
| 原型式繼承 | Object.create() | 無需構(gòu)造函數(shù),簡單 | 引用共享 | 對象淺拷貝 |
| 寄生式繼承 | 工廠模式+增強對象 | 無需構(gòu)造函數(shù),可增強對象 | 方法不能復用,引用共享 | 為對象添加功能 |
| 寄生組合繼承 | 借用構(gòu)造 + 寄生式繼承父類原型 | 近乎完美,只調(diào)用一次父類構(gòu)造函數(shù) | 實現(xiàn)稍復雜 | 理想的繼承范式 |
現(xiàn)代開發(fā)建議:
直接使用 ES6 的 class 和 extends 關(guān)鍵字。它們的語法更清晰、更易于理解,并且其底層實現(xiàn)的就是寄生組合式繼承這種最理想的方式。你不再需要手動處理原型鏈,避免了出錯的可能。
javascript
// ES6 的寫法,底層是寄生組合式繼承
class Parent {
constructor(name) {
this.name = name;
this.colors = ['red', 'blue'];
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, type) {
super(name); // 相當于 Parent.call(this, name)
this.type = type;
}
sayType() {
console.log(this.type);
}
}到此這篇關(guān)于JavaScript六種繼承方式的文章就介紹到這了,更多相關(guān)JS六種繼承方式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
將input框中輸入內(nèi)容顯示在相應的div中【三種方法可選】
本篇文章主要介紹了在input框中輸入內(nèi)容,會相應的顯示在下面的div中的不同做法:js方法;jQuery方法;AngularJs方法,具有很好的參考價值。下面跟著小編一起來看下吧2017-05-05

