引用GitHub 上 ltadpoles的前端面試
前言
作為 JavaScript 中最重要的內容之一,繼承問題一直是我們關注的重點。那么你是否清晰地知道它的原理以及各種實現方式呢
閱讀這篇文章,你將知道:
- 什么是繼承
- 實現繼承有哪幾種方式
- 它們各有什么特點
這里默認你已經清楚的知道構造函數、實例和原型對象之間的關系,如果並不是那么清晰,那么推薦你先閱讀這篇文章 -- JavaScript 中的原型與原型鏈
如果文章中有出現紕漏、錯誤之處,還請看到的小伙伴多多指教,先行謝過
以下↓
概念
繼承(inheritance)是面向對象軟件技術當中的一個概念。如果一個類別
B繼承自另一個類別A,就把這個B稱為A的子類,而把A稱為B的父類別也可以稱A是B的超類。繼承可以使得子類具有父類別的各種屬性和方法,而不需要再次編寫相同的代碼 ...更多
通過這些概念和圖示我們不難知道繼承可以在我們的開發中帶來的便捷,那么在 JavaScript 中如何去實現繼承呢?
繼承實現方式
原型鏈繼承
利用原型讓一個引用類型繼承另一個引用類型的屬性和方法
function SuperType() { this.name = 'tt'; } SuperType.prototype.sayName = function() { return this.name } function SubType() { this.name = 'oo'; } SubType.prototype = new SuperType() var instance = new SubType() instance.sayName() // oo instance instanceof SubType // true instance instanceof SuperType // ture
以上的試驗中,我們創建了兩個構造函數 SuperType 和 SubType ,並且讓 SubType 的原型指向 SuperType,SubType 也就繼承了 SuperType 原型對象中的方法。所以在創建 instance 實例的時候,實例本身也就具有了 SuperType 中的方法,並且都處在它們的原型鏈中
SubType.prototype.constructor == SubType // false SubType.prototype.constructor == SuperType // true
需要注意的是:這個時候 SubType.prototype.constructor 是指向 SuperType 的,相當於重寫了 SubType 的原型對象。
用一張圖表示:
SubType.prototype相當於SuperType的實例存在的,所以SubType.prototype.constructor就指向SuperType
原型繼承的特點
優點:
- 簡單、易於實現
- 父類新增原型方法/原型屬性,子類都能訪問到
- 非常純粹的繼承關系,實例是子類的實例,也是父類的實例
缺點:
- 無法實現多繼承
- 想要為子類
SubType添加原型方法,就必須在new SuperType之后添加(會覆蓋) - 來自原型對象的所有屬性被所有實例共享(引用類型的值修改會反映在所有實例上面)
- 創建子類實例時,無法向父類構造函數傳參
借用構造函數
在子類構造函數的內部調用超類型構造函數,通過
apply和call實現
function SuperType(name) { this.name = name; this.colors = ['red', 'orange', 'black']; } function SubType() { SuperType.call(this, 'tt'); } var instance = new SubType() var instance1 = new SubType() instance.colors // ['red', 'orange', 'black'] instance.name // tt instance.colors.push('green'); instance.colors // ['red', 'orange', 'black', 'green'] instance1.colors // ['red', 'orange', 'black']
借用構造函數的特點
優點:
- 解決了原型鏈繼承不能傳參的問題
- 子類實例共享父類引用屬性的問題
- 可以實現多繼承(
call可以指定不同的超類)
缺點:
- 實例並不是父類的實例,只是子類的實例
- 只能繼承父類的實例屬性和方法,不能繼承原型屬性/方法
- 無法實現函數復用
組合繼承
偽經典繼承(最常用的繼承模式):將原型鏈和借用構造函數的技術組合到一起。使用原型鏈實現對原型屬性和方法的繼承,通過構造函數來實現對實例屬性的繼承
function SuperType(name) { this.name = name; this.colors = ['red', 'orange', 'black']; } SuperType.prototype.sayName = function() { return this.name } function SubType() { SuperType.call(this, 'tt'); this.name = 'oo'; } // 這里的 SubType.prototype.constructor 還是指向 SuperType SubType.prototype = new SuperType(); var instance = new SubType(); var instance1 = new SubType(); instance.name // oo instance.sayName() // oo instance.colors.push('green'); instance.colors // ['red', 'orange', 'black', 'green'] instance1.colors // ['red', 'orange', 'black']
組合繼承的特點
優點:
- 可以繼承實例屬性/方法,也可以繼承原型屬性/方法
- 不存在引用屬性共享問題
- 可傳參
- 函數可復用
缺點:
- 調用了兩次父類構造函數,生成了兩份實例(子類實例將子類原型上的那份屏蔽了)
原型式繼承
借助原型鏈可以基於已有的對象創建新對象,同時還不必因此創建自定義類型
function obj(o) { function F(){} F.prototype = o; return new F(); } var person = { name: 'tt', age: 18, colors: ['red', 'green'] } var instance = obj(person); var instance1 = obj(person); instance.colors.push('black'); instance.name // tt instance.colors // ['red', 'green', 'black'] instance1.colors // ['red', 'green', 'black']
創建一個臨時的構造函數,然后將傳入的對象當做這個構造函數的原型對象,最后返回這個臨時構造函數的新實例。實際上,就是對傳入的對象進行了一次淺復制
ES5 通過新增 Object.create() 規范化了原型式繼承
更多 Object.create()語法請點擊 這里
原型式繼承特點
優點:
- 支持多繼承(傳入的對象不同)
- 不需要興師動眾的創建很多構造函數
缺點: 和原型鏈繼承基本一致,效率較低,內存占用高(因為要拷貝父類的屬性)
寄生式繼承
創建一個僅用於封裝繼承過程的函數,在函數內部對這個對象進行改變,最后返回這個對象
function createAnother(obj) { var clone = Object(obj); clone.sayHi = function() { alert('Hi'); } return clone } var person = { name: 'tt', age: 18, friends: ['oo', 'aa', 'cc'], sayName() { return this.name } } var instance = createAnother(person) var instance1 = createAnother(person) instance.friends.push('yy') instance.name // 'tt' instance.sayHi() // Hi instance.friends // ["oo", "aa", "cc", "yy"] instance1.friends // ["oo", "aa", "cc", "yy"]
寄生式繼承的特點
優點:
- 支持多繼承
缺點:
- 實例並不是父類的實例,只是子類的實例
- 不能實現復用(與構造函數相似)
- 實例之間會互相影響
寄生組合繼承
借用構造函數來繼承屬性,通過原型鏈的混成形式來繼承方法。通過寄生方式,砍掉父類的實例屬性,這樣,在調用兩次父類的構造的時候,就不會初始化兩次實例方法/屬性,避免的組合繼承的缺點
function inherit(subType, superType) { var obj = Object(superType.prototype); // 創建對象 obj.constructor = subType; // 指定constructor subType.prototype = obj; // 指定對象 } function SuperType(name) { this.name = name; this.colors = ['red', 'orange', 'black']; } SuperType.prototype.sayName = function() { return this.name } function SubType() { SuperType.call(this, 'tt'); this.name = 'oo'; } inherit(SubType, SuperType) var instance = new SubType() instance.name // oo instance.sayName // oo instance instanceof SubType // true instance instanceof SuperType // true SubType.prototype.constructor == SubType // true
寄生組合繼承的特點
堪稱完美,只是實現稍微復雜一點
后記
作為 JavaScript 最重要的概念之一,對於繼承實現的方式方法以及它們之間的差異我們還是很有必要了解的。


