總所周知,繼承是全部OO語言中都擁有的一個共性。在JavaScript中,它的繼承機制與其餘OO語言有着很大的不一樣,儘管ES6爲咱們提供了像面向對象繼承同樣的語法糖,可是其底層依然是構造函數,因此理解繼承的底層原理很是重要,因此今天讓咱們來探討一下JavaScript中的繼承機制。javascript
要理解繼承,必須理解JavaScript中的原型與原型鏈,我在以前的上一篇文章對原型進行了深刻的探討,有興趣的小夥伴能夠看看~前端
《理解原型與原型鏈》java
在JavaScript中,有六種主要常見的繼承方式,下面我會對每一種繼承方式進行分析並總結它們的優缺點。數組
在JavaScript中,實現繼承主要是依靠原型鏈來實現的。其基本思想是是利用原型讓一個引用類型繼承另外一個引用類型的屬性和方法。app
讓咱們簡單回顧一下構造函數、原型和實例的關係:每一個構造函數都有一個原型對象prototype,原型對象都包含一個指向構造函數的指針constructor,而實例都包含一個指向原型對象的內部指針__proto__函數
假如咱們讓原型對象等於另外一個類型的實例,結果會怎麼樣呢?讓咱們來看下面這段代碼。post
function Father() { this.name = 'zhang'; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 繼承了Father Son.prototype = new Father(); Son.prototype.sayAge = function() { console.log(this.age); } const xiaoming = new Son(); console.log(xiaoming.sayName()) // 'zhang'
以上代碼,Son繼承了Father,而繼承是經過建立Father的實例,並將Son.prototype指向new出來的Father實例。實現的本質是重寫了原型對象,待之是一個新類型的實例,也就是說,原來存在於Father構造函數中的全部屬性和方法,如今也存在於Son.prototype中。學習
經過上圖可知,咱們沒有使用Son默認提供的原型,而是給它換了一個新原型,這個原型就是Father的實例,其內部還有一個指針,指向Father的原型。因爲Son的原型被重寫了,因此xiaoming這個實例的constructor屬性如今指向的是Father。一句話總結就是Son繼承了Father,而Father繼承Object,當調用xiaoming.toString()方法時,其實是調用Object.prototype中的toString方法。this
注意:給子類原型添加方法的代碼必定要放到替換原型的語句以後prototype
還有一點須要提醒各位小夥伴們,在使用原型鏈繼承時,千萬不能使用對象字面量建立原型方法,由於這樣作會重寫原型鏈,來看下面這段代碼。
function Father() { this.name = 'zhang'; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 繼承了Father Son.prototype = new Father(); Son.prototype = { sayAge: function() { console.log(this.age) } } const xiaoming = new Son(); console.log(xiaoming.sayName()) // '報錯'
使用對象字面量建立原型方法,會切斷Father與Son之間的繼承關係哦~
子類型的實例對象擁有超類型的所有屬性和方法。
我在上面的那篇文章提到過,包含引用類型值的原型屬性會被全部實例共享。在經過原型實現繼承時,原型實際上會變成另外一個類型的實例,原先的實例屬性也就瓜熟蒂落地變成了如今的原型屬性了。
function Father() { this.cars = ['奔馳', '寶馬', '蘭博基尼']; } Father.prototype.sayName = function() { console.log(this.name); } function Son() { this.age = 18; } // 繼承了Father Son.prototype = new Father(); const xiaoming = new Son(); xiaoming.cars.push('五菱宏光'); console.log(xiaoming.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光' const xiaohong = new Son(); console.log(xiaohong.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光'
能夠從上述代碼中發現,當Father中的屬性是引用類型的時候,固然Father的每一個實例都會有各自的數組cars屬性。當Son繼承Father以後,Son.prototype就變成了Father的一個實例,結果就是xiaoming和xiaohong兩個實例對象共享一個cars屬性,這是在繼承中咱們不但願出現的。
第二個問題是建立Son的實例時,不能向Father的構造函數中傳遞參數,也就是說,沒有辦法在不影響全部對象實例的狀況下,給超類型的構造函數傳遞參數。
接下來我要將的第二種繼承方式是構造函數繼承,它能夠解決包含引用類型值所帶來的問題。
實現構造函數繼承的基本思想至關簡單,即在子類型構造函數的內部調用超類型構造函數。
讓咱們來看下面這段代碼:
function Father() { this.cars = ['奔馳', '寶馬', '蘭博基尼']; } function Son() { // 繼承Father Father.call(this); } const xiaoming = new Son(); xiaoming.cars.push('五菱宏光'); console.log(xiaoming.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光' const xiaohong = new Son(); console.log(xiaohong.cars); //'奔馳, 寶馬, 蘭博基尼'
經過使用call()
方法(或apply()
方法),在建立xiaoming實例的同時,調用了Father構造函數,這樣一來,就會在Son的實例對象上執行Father構造函數所定義的全部對象初始化代碼,所以xiaoming和xiaohong就具備屬於本身的cars屬性了。
構造函數繼承還有一個優勢是能夠給超類型構造函數傳參,讓咱們來看下面這段代碼。
function Father(name) { this.name = name; } function Son(name, age) { Father(this, name); this.age = age; } const xiaoming = new Son('小明', 19); console.log(xiaoming.name); //'小明' console.log(xiaoming.age); //19
咱們建立了xiaoming實例並傳遞兩個參數name和age,name參數經過調用Father構造函數傳遞參數給了Father構造函數中的name,所以xiaoming實例擁有name和age兩個實例屬性。
能夠在子類型構造函數中向超類型構造函數傳參;子類型構造函數建立的對象都擁有各自的屬性和方法(引用類型)
很明顯,方法都在構造函數中定義的話,函數複用就無從談起了,所以構造函數繼承不多單獨使用。接下來介紹的這種繼承方式,經過原型鏈和構造函數結合實現的繼承,叫作組合繼承。
組合繼承的基本思路是使用原型鏈實現對原型屬性和方法的繼承,而經過借用構造函數來實現對實例屬性的繼承。
使用組合繼承的優勢是即經過在原型上定義方法實現了函數複用,又可以保證每一個實例都有它本身的屬性,來看下面這段代碼。
function Father(name) { this.name = name; this.cars = ['奔馳', '寶馬', '蘭博基尼']; } Father.prototype.sayName = function() { console.log(this.name); } function Son(name, age) { // 繼承屬性 Father.call(this, name); //第二次調用Father() this.age = age; } // 繼承方法 Son.prototype = new Father(); //第一次調用Father() Son.prototype.constructor = Son; Son.prototype.sayAge = function() { console.log(this.age); } const xiaoming = new Son('xiaoming', 18); xiaoming.cars.push('五菱宏光'); console.log(xiaoming.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光' xiaoming.sayName(); //'xiaoming' xiaoming.sayAge(); //18 const xiaohong = new Son('xiaohong', 20); console.log(xiaohong.cars); //'奔馳, 寶馬, 蘭博基尼' xiaohong.sayName(); //'xiaohong' xiaohong.sayAge(); //20 console.log(xiaoming instanceof Son) //true console.log(xiaoming instanceof Father) //true console.log(xiaoming instanceof Object) //true
組合繼承避免了原型鏈繼承和構造函數繼承的缺陷,融合它們的優勢,成爲JavaScript中最經常使用的繼承模式。
組合繼承最大的問題就是不管什麼狀況下,都會調用兩次超類型構造函數。一次是在建立子類型原型的時候,另外一次是在子類型構造函數內部。
原型式繼承的就是藉助原型能夠基於已有的對象建立新對象。
咱們來看下面這段代碼。
function object(o) { function F() {} F.prototype = o; return new F(); } const person = { name: 'zhangsan', cars: ['奔馳', '寶馬', '蘭博基尼'] } const anotherPerson = object(person); anotherPerson.name = 'lisi'; anotherPerson.cars.push('五菱宏光'); console.log(anotherPerson.name); //'lisi' console.log(anotherPerson.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光' const yetAnotherPerson = object(person); yetAnotherPerson.name = 'wangwu'; console.log(yetAnotherPerson.name); //'wangwu' console.log(yetAnotherPerson.cars); //'奔馳, 寶馬, 蘭博基尼, 五菱宏光'
object()
其實是對對象的一次淺複製,實現原型式繼承的前提是要求你必須有一個對象能夠做爲另外一個對象的基礎。
ES5新增了Object.create()
方法,這個方法規範化了原型式繼承。這個方法我在這裏很少介紹,感興趣的小夥伴能夠參考MDN的說明文檔Object.create()
若是隻想讓一個對象與另一個對象保持相似的狀況下,原型式繼承能夠徹底勝任。
原型式繼承的缺點相信各位小夥伴們已經看出來了,包含引用類型值的屬性始終都會共享相應的值,就像使用原型鏈繼承同樣。
寄生式(parasitic)繼承是與原型式繼承緊密相關的一種思路,即建立一個僅用於封裝繼承過程的函數,該函數在內部以某種方式來加強對象,最後再返回對象。
廢話很少說,讓咱們來看下面這段代碼。
function createAnother(original) { const clone = Object.create(original); clone.sayHi = function() { console.log('hi'); } return clone; } const person = { name: 'zhangsan', cars: ['奔馳', '寶馬', '蘭博基尼'] } const anotherPerson = createAnother(person); anotherPerson.sayHi(); //'hi' const yetAnotherPerson = createAnother(person); yetAnotherPerson.sayHi(); //'hi' console.log(anotherPerson.sayHi == yetAnotherPerson.sayHi) //false
這個例子中,封裝了一個createAnother
的函數,這個函數接收一個參數,也就是將要做爲新對象的基礎對象,咱們能夠看到,anotherPerson和yetAnotherPerson兩個對象擁有各自的sayHi方法。
在主要考慮對象而不是自定義類型和構造函數的狀況下,寄生式繼承也是一種有用的模式。
繼承的對象都擁有各自的屬性和方法(引用類型)。
使用寄生式繼承來爲對象添加函數,會因爲不能作到函數複用而下降效率,這一點與構造函數繼承模式相似。
所謂寄生組合式繼承,就是經過構造函數來繼承屬性,經過原型鏈的混成形式來繼承方法。其背後的基本思路是:沒必要爲了指定子類型的原型而調用超類型的構造函數,咱們所需的無非就是超類型原型的一個副本而已。
本質上,就是使用寄生式繼承來繼承超類型的原型,而後再將結果指定給子類型的原型。讓咱們來看下面這段代碼。
function inheritPrototype(Son, Father) { const prototype = Object.create(Father.prototype); prototype.constructor = Son; Son.prototype = prototype; } function Father(name) { this.name = name; this.cars = ['奔馳', '寶馬', '蘭博基尼']; } Father.prototype.sayName = function() { console.log(this.name); } function Son(name, age) { Father.call(this, name); //調用Father this.age = age; } inheritPrototype(Son, Father); Son.prototype.sayAge = function() { console.log(this.age); }
這個例子的高效率體如今它只調用了一次Father構造函數,而且所以避免在Son.prototype上面建立沒必要要、多餘的屬性。
寄生組合式繼承只調用了一次超類型構造函數,是被開發人員廣泛認爲是引用類型最理想的繼承範式。
前端的學習之路還有很長很長,這篇文章只不過是冰山一角,但願前端cc寫的這篇文章能給小夥伴們帶來新的知識拓展,願前端cc與各位前端小夥伴們在前端生涯中一塊兒共同成長,衝鴨!