詳解JavaScript中8 種不同的繼承實現(xiàn)方式
前言
在 JavaScript 中,繼承是實現(xiàn)代碼復用和構建復雜對象關系的重要機制。雖然 JavaScript 是一門基于原型的語言,不像傳統(tǒng)面向對象語言那樣有類的概念,但它提供了多種實現(xiàn)繼承的方式。本文將詳細介紹 JavaScript 中 8 種不同的繼承實現(xiàn)方式,每種方式都會配有代碼示例和詳細解釋,最后還會通過流程圖比較各種繼承方式的特點。
1. 原型鏈繼承
原型鏈繼承是 JavaScript 中最基本的繼承方式,它利用原型讓一個引用類型繼承另一個引用類型的屬性和方法。
function Parent() { this.name = 'Parent'; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child() { this.childName = 'Child'; } // 關鍵步驟:將Child的原型指向Parent的實例 Child.prototype = new Parent(); var child1 = new Child(); console.log(child1.getName()); // "Parent" console.log(child1.childName); // "Child" // 問題:引用類型的屬性會被所有實例共享 child1.colors.push('black'); var child2 = new Child(); console.log(child2.colors); // ["red", "blue", "green", "black"]
特點:
- 簡單易實現(xiàn)
- 父類新增原型方法/屬性,子類都能訪問到
- 無法實現(xiàn)多繼承
- 來自原型對象的引用屬性被所有實例共享
- 創(chuàng)建子類實例時,無法向父類構造函數(shù)傳參
2. 構造函數(shù)繼承(經(jīng)典繼承)
通過在子類構造函數(shù)中調用父類構造函數(shù)實現(xiàn)繼承。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { // 關鍵步驟:在子類構造函數(shù)中調用父類構造函數(shù) Parent.call(this, name); this.age = age; } var child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.name); // "Tom" console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] var child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"] // 問題:無法繼承父類原型上的方法 console.log(child1.getName); // undefined
特點:
- 解決了原型鏈繼承中引用類型共享的問題
- 可以在子類構造函數(shù)中向父類構造函數(shù)傳參
- 可以實現(xiàn)多繼承(call多個父類對象)
- 只能繼承父類實例屬性和方法,不能繼承原型屬性和方法
- 無法實現(xiàn)函數(shù)復用,每個子類都有父類實例函數(shù)的副本,影響性能
3. 組合繼承(最常用)
組合繼承結合了原型鏈繼承和構造函數(shù)繼承的優(yōu)點。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { // 構造函數(shù)繼承 - 第二次調用Parent() Parent.call(this, name); this.age = age; } // 原型鏈繼承 - 第一次調用Parent() Child.prototype = new Parent(); // 修正constructor指向 Child.prototype.constructor = Child; Child.prototype.getAge = function() { return this.age; }; var child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.name); // "Tom" console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 var child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"] console.log(child2.getName()); // "Jerry" console.log(child2.getAge()); // 20
特點:
- 融合原型鏈繼承和構造函數(shù)繼承的優(yōu)點
- 既是子類的實例,也是父類的實例
- 不存在引用屬性共享問題
- 可傳參
- 函數(shù)可復用
- 調用了兩次父類構造函數(shù),生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
4. 原型式繼承
借助原型可以基于已有對象創(chuàng)建新對象。
function object(o) { function F() {} F.prototype = o; return new F(); } var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var 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"]
ES5 規(guī)范化了原型式繼承,新增了 Object.create()
方法:
var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = Object.create(person); anotherPerson.name = 'Greg'; anotherPerson.friends.push('Rob'); var yetAnotherPerson = Object.create(person, { name: { value: 'Linda' } }); yetAnotherPerson.friends.push('Barbie'); console.log(person.friends); // ["Shelby", "Court", "Van", "Rob", "Barbie"]
特點:
- 不需要單獨創(chuàng)建構造函數(shù)
- 本質是對給定對象執(zhí)行淺復制
- 適用于不需要單獨創(chuàng)建構造函數(shù),但仍需要在對象間共享信息的場合
- 同原型鏈繼承一樣,包含引用類型的屬性會被共享
5. 寄生式繼承
創(chuàng)建一個僅用于封裝繼承過程的函數(shù),在函數(shù)內部增強對象。
function createAnother(original) { var clone = Object.create(original); // 通過調用函數(shù)創(chuàng)建一個新對象 clone.sayHi = function() { // 以某種方式增強這個對象 console.log('Hi'); }; return clone; // 返回這個對象 } var person = { name: 'Nicholas', friends: ['Shelby', 'Court', 'Van'] }; var anotherPerson = createAnother(person); anotherPerson.sayHi(); // "Hi"
特點:
- 基于原型式繼承
- 增強了對象
- 無法實現(xiàn)函數(shù)復用
- 同原型式繼承一樣,引用類型屬性會被共享
6. 寄生組合式繼承(最理想)
通過借用構造函數(shù)繼承屬性,通過原型鏈混成形式繼承方法。
function inheritPrototype(child, parent) { var prototype = Object.create(parent.prototype); // 創(chuàng)建父類原型的副本 prototype.constructor = child; // 修正constructor指向 child.prototype = prototype; // 將副本賦值給子類原型 } function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function() { return this.name; }; function Child(name, age) { Parent.call(this, name); this.age = age; } // 關鍵步驟:避免調用Parent構造函數(shù),直接使用父類原型 inheritPrototype(Child, Parent); Child.prototype.getAge = function() { return this.age; }; var child1 = new Child('Tom', 18); var child2 = new Child('Jerry', 20); console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 console.log(child2.getName()); // "Jerry" console.log(child2.getAge()); // 20
特點:
- 只調用一次父類構造函數(shù)
- 避免在子類原型上創(chuàng)建不必要的屬性
- 原型鏈保持不變
- 能夠正常使用 instanceof 和 isPrototypeOf
- 是引用類型最理想的繼承方式
7. ES6 Class 繼承
ES6 引入了 class 語法糖,使得繼承更加清晰易讀。
class Parent { constructor(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } getName() { return this.name; } } class Child extends Parent { constructor(name, age) { super(name); // 調用父類的constructor this.age = age; } getAge() { return this.age; } } const child1 = new Child('Tom', 18); child1.colors.push('black'); console.log(child1.getName()); // "Tom" console.log(child1.getAge()); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] const child2 = new Child('Jerry', 20); console.log(child2.colors); // ["red", "blue", "green"]
特點:
- 語法更加清晰易讀
- 底層實現(xiàn)仍然是基于原型
- 通過 extends 實現(xiàn)繼承
- 子類必須在 constructor 中調用 super(),否則新建實例時會報錯
- ES6 的繼承機制完全不同,實質是先創(chuàng)造父類的實例對象 this(所以必須先調用 super 方法),然后再用子類的構造函數(shù)修改 this
8. 混入方式繼承(多繼承)
JavaScript 本身不支持多繼承,但可以通過混入(Mixin)的方式實現(xiàn)類似功能。
function extend(target, ...sources) { sources.forEach(source => { for (let key in source) { if (source.hasOwnProperty(key)) { target[key] = source[key]; } } // 支持Symbol屬性 const symbols = Object.getOwnPropertySymbols(source); symbols.forEach(symbol => { target[symbol] = source[symbol]; }); }); return target; } const canEat = { eat() { console.log(`${this.name} is eating.`); } }; const canWalk = { walk() { console.log(`${this.name} is walking.`); } }; const canSwim = { swim() { console.log(`${this.name} is swimming.`); } }; function Person(name) { this.name = name; } // 將多個mixin混入Person的原型 extend(Person.prototype, canEat, canWalk); const person = new Person('John'); person.eat(); // "John is eating." person.walk(); // "John is walking." // person.swim(); // 報錯,沒有swim方法 function Fish(name) { this.name = name; } extend(Fish.prototype, canEat, canSwim); const fish = new Fish('Nemo'); fish.eat(); // "Nemo is eating." fish.swim(); // "Nemo is swimming." // fish.walk(); // 報錯,沒有walk方法
ES6 中可以使用 Object.assign 簡化混入:
class Person { constructor(name) { this.name = name; } } Object.assign(Person.prototype, canEat, canWalk); class Fish { constructor(name) { this.name = name; } } Object.assign(Fish.prototype, canEat, canSwim);
特點:
- 可以實現(xiàn)類似多繼承的功能
- 靈活性強,可以按需組合功能
- 不是真正的繼承,而是屬性拷貝
- 可能會導致命名沖突
- 無法使用 instanceof 檢查混入的功能
繼承方式比較流程圖
總結
JavaScript 提供了多種實現(xiàn)繼承的方式,每種方式都有其適用場景和優(yōu)缺點:
- 原型鏈繼承:簡單但引用類型屬性會被共享
- 構造函數(shù)繼承:可解決引用共享問題但無法繼承原型方法
- 組合繼承:最常用的繼承方式,但會調用兩次父類構造函數(shù)
- 原型式繼承:適用于基于已有對象創(chuàng)建新對象
- 寄生式繼承:增強對象但無法函數(shù)復用
- 寄生組合繼承:最理想的繼承方式,高效且完整
- ES6 Class繼承:語法糖,底層仍是原型繼承
- 混入方式:實現(xiàn)類似多繼承的功能
在實際開發(fā)中,ES6 的 class 語法是最推薦的方式,它語法簡潔,易于理解,且底層實現(xiàn)高效。對于需要兼容舊瀏覽器的項目,可以使用寄生組合式繼承作為替代方案。
理解這些繼承方式的原理和區(qū)別,有助于我們在不同場景下選擇最合適的實現(xiàn)方式,寫出更優(yōu)雅、高效的 JavaScript 代碼。
以上就是詳解JavaScript中8 種不同的繼承實現(xiàn)方式的詳細內容,更多關于JavaScript實現(xiàn)繼承的資料請關注腳本之家其它相關文章!
相關文章
JavaScript利用canvas實現(xiàn)炫酷的碎片切圖效果
這篇文章主要和大家分享一個炫酷的碎片式切圖效果,本文主要利用canvas來實現(xiàn),代碼量不多,但有些地方還是需要花點時間去理解的,感興趣的可以學習一下2022-10-10amd、cmd、esmodule、commonjs區(qū)別詳解
本文主要介紹了amd、cmd、esmodule、commonjs區(qū)別詳解,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-04-04