上篇文章詳細解析了原型、原型鏈的相關知識點,這篇文章講的是和原型鏈有密切關聯的繼承,它是前端基礎中很重要的一個知識點,它對於代碼複用來講很是有用,本篇將詳細解析JS中的各類繼承方式和優缺點進行,但願看完本篇文章可以對繼承以及相關概念理解的更爲透徹。html
call
的相關知識:js基礎-面試官想知道你有多理解call,apply,bind?git
維基百科):繼承可使得子類具備父類別的各類屬性和方法,而不須要再次編寫相同的代碼。
繼承是一個類從另外一個類獲取方法和屬性的過程。es6
PS:或者是多個類github
記住這個概念,你會發現JS中的繼承都是在實現這個目的,差別是它們的實現方式不一樣。
複製父類的屬性和方法來重寫子類原型對象。web
function fatherFn() { this.some = '父類的this屬性'; } fatherFn.prototype.fatherFnSome = '父類原型對象的屬性或者方法'; // 子類 function sonFn() { this.obkoro1 = '子類的this屬性'; } // 核心步驟:重寫子類的原型對象 sonFn.prototype = new fatherFn(); // 將fatherFn的實例賦值給sonFn的prototype sonFn.prototype.sonFnSome = '子類原型對象的屬性或者方法' // 子類的屬性/方法聲明在後面,避免被覆蓋 // 實例化子類 const sonFnInstance = new sonFn(); console.log('子類的實例:', sonFnInstance);
fatherFn
經過this聲明的屬性/方法都會綁定在new
期間建立的新對象上。father.prototype
,經過原型鏈的屬性查找到father.prototype
的屬性和方法。new
作了什麼:new在本文出現屢次,new也是JS基礎中很重要的一塊內容,不少知識點會涉及到new,不太理解的要多看幾遍。
__proto__
)指向函數的prototype
對象。返回其餘對象會致使獲取不到構造函數的實例,很容易所以引發意外的問題!
咱們知道了fatherFn
的this
和prototype
的屬性/方法都跟new
期間建立的新對象有關係。面試
若是在父類中返回了其餘對象(new
的第四點),其餘對象沒有父類的this
和prototype
,所以致使原型鏈繼承失敗。數組
咱們來測試一下,修改原型鏈繼承中的父類fatherFn
:babel
function fatherFn() { this.some = '父類的this屬性'; console.log('new fatherFn 期間生成的對象', this) return [ '數組對象', '函數對象', '日期對象', '正則對象', '等等等', '都不會返回new期間建立的新對象' ] }
PS: 本文中構造調用函數都不能返回其餘函數,下文再也不說起該點。app
這種方式很容易在不經意間,清除/覆蓋了原型對象原有的屬性/方法,不應爲了稍微簡便一點,而使用這種寫法。
有些人在須要在原型對象上建立多個屬性和方法,會使用對象字面量的形式來建立:
sonFn.prototype = new fatherFn(); // 子類的prototype被清空後 從新賦值, 致使上一行代碼失效 sonFn.prototype = { sonFnSome: '子類原型對象的屬性', one: function() {}, two: function() {}, three: function() {} }
還有一種常見的作法,該方式會致使函數原型對象的屬性constructor
丟失:
function test() {} test.prototype = { ... }
this
聲明的屬性被全部實例共享緣由是:實例化的父類(sonFn.prototype = new fatherFn()
)是一次性賦值到子類實例的原型(sonFn.prototype
)上,它會將父類經過this
聲明的屬性也在賦值到sonFn.prototype
上。
值得一提的是:不少博客中說,引用類型的屬性被全部實例共享,一般會用數組來舉例,實際上數組以及其餘父類經過this
聲明的屬性也只是經過 原型鏈查找去獲取子類實例的原型(sonFn.prototype
)上的值。
這種模式父類的屬性、方法一開始就是定義好的,沒法向父類傳參,不夠靈活。
sonFn.prototype = new fatherFn()
function fatherFn(...arr) { this.some = '父類的this屬性'; this.params = arr // 父類的參數 } fatherFn.prototype.fatherFnSome = '父類原型對象的屬性或者方法'; function sonFn(fatherParams, ...sonParams) { fatherFn.call(this, ...fatherParams); // 核心步驟: 將fatherFn的this指向sonFn的this對象上 this.obkoro1 = '子類的this屬性'; this.sonParams = sonParams; // 子類的參數 } sonFn.prototype.sonFnSome = '子類原型對象的屬性或者方法' let fatherParamsArr = ['父類的參數1', '父類的參數2'] let sonParamsArr = ['子類的參數1', '子類的參數2'] const sonFnInstance = new sonFn(fatherParamsArr, ...sonParamsArr); // 實例化子類 console.log('借用構造函數子類實例', sonFnInstance)
聲明類,組織參數等,只是輔助的上下文代碼,核心是借用構造函數使用call
作了什麼:
一經調用call/apply
它們就會當即執行函數,並在函數執行時改變函數的this
指向
fatherFn.call(this, ...fatherParams);
call
調用父類,fatherFn
將會被當即執行,而且將fatherFn
函數的this指向sonFn
的this
。fatherFn
使用this聲明的函數都會被聲明到sonFn
的this
對象下。new
期間建立的新對象,返回該新對象。fatherFn.prototype
沒有任何操做,沒法繼承。該對象的屬性爲:子類和父類聲明的this
屬性/方法,它的原型是
PS: 關於call/apply/bind的更多細節,推薦查看個人博客:[js基礎-面試官想知道你有多理解call,apply,bind?[不看後悔系列]](https://juejin.im/post/5d469e...
優勢:
this
聲明的屬性會在全部實例共享的問題。缺點:
this
聲明的屬性/方法,不能繼承父類prototype
上的屬性/方法。prototype
,因此每次子類實例化都要執行父類函數,從新聲明父類this
裏所定義的方法,所以方法沒法複用。原理:使用原型鏈繼承(new
)將this
和prototype
聲明的屬性/方法繼承至子類的prototype
上,使用借用構造函數來繼承父類經過this
聲明屬性和方法至子類實例的屬性上。
function fatherFn(...arr) { this.some = '父類的this屬性'; this.params = arr // 父類的參數 } fatherFn.prototype.fatherFnSome = '父類原型對象的屬性或者方法'; function sonFn() { fatherFn.call(this, '借用構造繼承', '第二次調用'); // 借用構造繼承: 繼承父類經過this聲明屬性和方法至子類實例的屬性上 this.obkoro1 = '子類的this屬性'; } sonFn.prototype = new fatherFn('原型鏈繼承', '第一次調用'); // 原型鏈繼承: 將`this`和`prototype`聲明的屬性/方法繼承至子類的`prototype`上 sonFn.prototype.sonFnSome = '子類原型對象的屬性或者方法' const sonFnInstance = new sonFn(); console.log('組合繼承子類實例', sonFnInstance)
從圖中能夠看到fatherFn
經過this
聲明的屬性/方法,在子類實例的屬性上,和其原型上都複製了一份,緣由在代碼中也有註釋:
this
和prototype
聲明的屬性/方法繼承至子類的prototype
上。優勢:
完整繼承(又不是不能用),解決了:
this
聲明屬性/方法被子類實例共享的問題(原型鏈繼承的問題)this
聲明的屬性,實例根據原型鏈查找規則,每次都會prototype
聲明的屬性/方法沒法繼承的問題(借用構造函數的問題)。缺點:
new fatherFn()
和fatherFn.call(this)
),形成必定的性能損耗。this
聲明的屬性/方法,生成兩份的問題。Object.create()
)如下是Object.create()
的模擬實現,使用Object.create()
能夠達成一樣的效果,基本上如今都是使用Object.create()
來作對象的原型繼承。
function cloneObject(obj){ function F(){} F.prototype = obj; // 將被繼承的對象做爲空函數的prototype return new F(); // 返回new期間建立的新對象,此對象的原型爲被繼承的對象, 經過原型鏈查找能夠拿到被繼承對象的屬性 }
PS:上面Object.create()
實現原理能夠記一下,有些公司可能會讓你講一下它的實現原理。
let oldObj = { p: 1 }; let newObj = cloneObject(oldObj) oldObj.p = 2 console.log('oldObj newObj', oldObj, newObj)
優勢: 兼容性好,最簡單的對象繼承。
缺點:
oldObj
)是實例對象(newObj
)的原型,多個實例共享被繼承對象的屬性,存在篡改的可能。建立一個 僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後返回對象。
function createAnother(original){ var clone = cloneObject(original); // 繼承一個對象 返回新函數 // do something 以某種方式來加強對象 clone.some = function(){}; // 方法 clone.obkoro1 = '封裝繼承過程'; // 屬性 return clone; // 返回這個對象 }
使用場景:專門爲對象來作某種固定方式的加強。
call
)來繼承父類this聲明的屬性/方法 function fatherFn(...arr) { this.some = '父類的this屬性'; this.params = arr // 父類的參數 } fatherFn.prototype.fatherFnSome = '父類原型對象的屬性或者方法'; function sonFn() { fatherFn.call(this, '借用構造繼承'); // 核心1 借用構造繼承: 繼承父類經過this聲明屬性和方法至子類實例的屬性上 this.obkoro1 = '子類的this屬性'; } // 核心2 寄生式繼承:封裝了son.prototype對象原型式繼承father.prototype的過程,而且加強了傳入的對象。 function inheritPrototype(son, father) { const fatherFnPrototype = Object.create(father.prototype); // 原型式繼承:淺拷貝father.prototype對象 father.prototype爲新對象的原型 son.prototype = fatherFnPrototype; // 設置father.prototype爲son.prototype的原型 son.prototype.constructor = son; // 修正constructor 指向 } inheritPrototype(sonFn, fatherFn) sonFn.prototype.sonFnSome = '子類原型對象的屬性或者方法' const sonFnInstance = new sonFn(); console.log('寄生組合式繼承子類實例', sonFnInstance)
寄生組合式繼承是最成熟的繼承方法, 也是如今最經常使用的繼承方法,衆多JS庫採用的繼承方案也是它。
寄生組合式繼承相對於組合繼承有以下優勢:
fatherFn
構造函數。子類的prototype只有子類經過prototype聲明的屬性/方法和父類prototype上的屬性/方法涇渭分明。
ES6繼承的原理跟寄生組合式繼承是同樣的。
ES6 extends
核心代碼:
這段代碼是經過babel在線編譯
成es5, 用於子類prototype原型式繼承父類prototype
的屬性/方法。
// 寄生式繼承 封裝繼承過程 function _inherits(son, father) { // 原型式繼承: 設置father.prototype爲son.prototype的原型 用於繼承father.prototype的屬性/方法 son.prototype = Object.create(father && father.prototype); son.prototype.constructor = son; // 修正constructor 指向 // 將父類設置爲子類的原型 用於繼承父類的靜態屬性/方法(father.some) if (father) { Object.setPrototypeOf ? Object.setPrototypeOf(son, father) : son.__proto__ = father; } }
另外子類是經過借用構造函數繼承(call
)來繼承父類經過this
聲明的屬性/方法,也跟寄生組合式繼承同樣。
本段摘自 阮一峯-es6入門文檔
由於子類沒有本身的this對象,因此必須先調用父類的super()方法。
在寄生組合式繼承中有一段以下一段修正constructor 指向的代碼,不少人對於它的做用以及爲何要修正它不太清楚。
son.prototype.constructor = son; // 修正constructor 指向
MDN的定義:返回建立實例對象的Object
構造函數的引用。
即返回實例對象的構造函數的引用,例如:
let instance = new sonFn() instance.constructor // sonFn函數
construct
的應用場景:當咱們只有實例對象沒有構造函數的引用時:
某些場景下,咱們對實例對象通過多輪導入導出,咱們不知道實例是從哪一個函數中構造出來或者追蹤實例的構造函數,較爲艱難。
這個時候就能夠經過實例對象的constructor
屬性來獲得構造函數的引用:
let instance = new sonFn() // 實例化子類 export instance; // 多輪導入+導出,致使sonFn追蹤很是麻煩,或者不想在文件中再引入sonFn let fn = instance.construct // do something: new fn() / fn.prototype / fn.length / fn.arguments等等
construct
指向的一致性:所以每次重寫函數的prototype都應該修正一下construct
的指向,以保持讀取construct
行爲的一致性。
繼承也是前端的高頻面試題,瞭解本文中繼承方法的優缺點,有助於更深入的理解JS繼承機制。除了組合繼承和寄生式繼承都是由其餘方法組合而成的,分塊理解會對它們理解的更深入。
建議多看幾遍本文,建個html
文件試試文中的例子,兩相結合更佳!
對prototype還不是很理解的同窗,能夠再看看:JS基礎-函數、對象和原型、原型鏈的關係
前端進階積累、公衆號、GitHub、wx:OBkoro一、郵箱:obkoro1@foxmail.com
以上2019/9/22
做者:OBKoro1
參考資料:
JS高級程序設計(紅寶書)6.3繼承