javascript
JavaScript六种继承方式的递进推演
1. 原型鏈繼承
function Parent1() {this.name = "Parent1"this.son = [1] } // 需要繼承的子類 function Child1() {this.age = 18 } // 修改Child1的原型對象 Child1.prototype = new Parent1() // 用new創(chuàng)建一個父類的匿名實例對象// Child1的實例對象可以獲取到自己構(gòu)造函數(shù)的原型對象(即new Parent1())的屬性和方法了 const c1 = new Child1() console.log(c1.name) // Parent1 console.log(c1.__proto__) // Parent1 { name: 'Parent1', son: [ 1 ] }不過這樣會存在一個潛在問題:
// 再創(chuàng)建一個子類的實例對象 const c2 = new Child1() c2.son.push(2) console.log(c1.son, c2.son) //[ 1, 2 ] [ 1, 2 ]可以看到修改c2的son屬性,c1的son屬性也跟著變了。這是因為兩個實例使用的是同一個原型對象,內(nèi)存空間是共享的。
當然如果換種方式修改是不會影響其他實例對象的:
c2.son = [1, 2] conosle.log(c1.son, c2.son) //[1], [1,2]2. 構(gòu)造函數(shù)繼承
function Parent2() {this.name = "parent2"this.son = [1] }function Child2() {// 執(zhí)行Parent構(gòu)造函數(shù),借用call修改this指向為子類Child2Parent2.call(this)this.age = 18 } const c1 = new Child2() console.log(c1.name) // Parent2// 不會有原型鏈繼承的問題了 const c2 = new Child2() c2.son.push(2) console.log(c1.son, c2.son) // [1], [1, 2]因為每次調(diào)用構(gòu)造函數(shù)就會開辟一塊新的內(nèi)存空間,所以是不會存在原型鏈繼承共用內(nèi)存空間的問題。
但是又會出現(xiàn)新的問題:
Parent2.prototype.getName = function() {return this.name } console.log(c2.getName()) // TypeError: c2.getName is not a function可以看到,構(gòu)造函數(shù)繼承中,子類無法繼承父類原型上的方法。
3. 組合繼承
function Parent3() {this.name = "parent3"this.son = [1]Parent3.prototype.getName = function() {return this.name} }function Child3() {// 這一步讓子類擁有了父類的實例屬性和方法Parent3.call(this) // 第二次調(diào)用Parent3this.age = 18 }// 手動掛上構(gòu)造器,同時指回自己的構(gòu)造函數(shù) // 這兩步讓子類擁有了父類原型的屬性和方法 Child3.prototype = new Parent3() // 第一次調(diào)用 Parent3() Child3.prototype.constructor = Child3 // 這一步不寫似乎也可以,但是為避免意料之外的其他問題,最好還是加上const c1 = new Child3() const c2 = new Child3() c1.son.push(2) console.log(c1.son, c2.son) // [ 1, 2 ] [ 1 ] 不會互相影響 console.log(c1.getName(), c2.getName()) // parent3 parent3結(jié)合上述兩種繼承方式的優(yōu)缺點,可以輕松的寫出這種組合繼承的方法。
思路很簡單:用構(gòu)造函數(shù)繼承實例屬性和方法,用原型鏈繼承原型屬性和方法。
這種方式是最JS中最常用的繼承方式,但是仍然存在問題:
缺點也很直觀:一次繼承需要調(diào)用兩次父類,這就造成了額外的性能開銷(多構(gòu)造了一次),如果父類的共有屬性和方法極多,那么會大大降低執(zhí)行效率。
4. 原型式繼承
Object.create(proto [,descriptors]) —— 利用給定的 proto 作為 [[Prototype]] 和可選的屬性描述來創(chuàng)建一個空對象。
可以用如下代碼表述其原理(來自第二篇參考文章,紅寶書中的一段引用):
function object(o) {function F() {}; //臨時構(gòu)造函數(shù)F.prototype = o; //傳入對象o作為臨時構(gòu)造函數(shù)的原型對象return new F(); //返回臨時構(gòu)造對象實例 } const parent4 = {name: "parent4",friends: ["p1", "p2", "p3"],getName: function() {return this.name;}};const s1 = Object.create(parent4);const s2 = Object.create(parent4);s1.name = "tom";s1.friends.push("jerry");s2.friends.push("lucy");console.log(s1.name); // tomconsole.log(s1.name === s1.getName()); // trueconsole.log(s2.name); // parent4console.log(s1.friends); // ["p1", "p2", "p3","jerry","lucy"]console.log(s2.friends); // ["p1", "p2", "p3","jerry","lucy"]根據(jù)我個人的研究,也可以用其他方式取代 Object.create() :
//(代碼接上) // 根據(jù)上面的原理代碼,我們可以知道,Object.create()本質(zhì)上最終修改的是對象的 [[prototype]] 隱藏屬性,這個屬性可以用__proto__獲取到: console.log(s1.__proto__ === parent4) // true//那么直接使用__proto__也是可以的: const s3 = {} s3.__proto__ = parent4//或者使用Object.setPrototypeOf(): const s4 = {} Object.setPrototypeOf(s4,parent4)這樣只是為了方便理解,這種繼承方式肯定是推薦使用Object.create()的。關(guān)于這三種方法可以參考下這篇文章:原型方法,沒有 proto 的對象
這種繼承方式的缺點也很明顯,因為Object.create方法實現(xiàn)的是淺拷貝,多個實例的引用類型屬性指向相同的內(nèi)存,存在篡改的可能。
5. 寄生式繼承
寄生式繼承也沒啥好說的,只是在上面繼承的基礎(chǔ)上做了點優(yōu)化(因此缺點也是一樣的),可以自主添加一些方法以增強功能:
const parent5 = {name: "parent5",friends: ["p1", "p2", "p3"],getName: function() {return this.name} }// 封裝一個繼承的方法 function inherit(original) {let inherit = Object.create(original);// 需要額外添加的方法inherit.getFriends = function() {return this.friends}return inherit }const person5 = clone(parent5)console.log(person5.getName()); // parent5 console.log(person5.getFriends()); // ["p1", "p2", "p3"]6. 寄生組合式繼承
寄生組合繼承利用寄繼承的核心方法:Object.create() 優(yōu)化了組合繼承會額外執(zhí)行一次父類構(gòu)造函數(shù)的問題:
// 整段代碼還是組合繼承的代碼: function Parent3() {this.name = "parent3"this.son = [1]Parent3.prototype.getName = function() {return this.name}// 可以加上代碼測試:// console.log(this) // 組合式繼承會多打印一次this,這個this是父類構(gòu)造函數(shù)即Parent3// 而寄生組合式繼承只會在創(chuàng)建實例對象時執(zhí)行一次,打印對應(yīng)的this即C1 } function Child3() {Parent3.call(this)this.age = 18 }// Child3.prototype = new Parent3() // 這里改用 Object.create 就可以減少組合繼承中多進行一次構(gòu)造的過程 Child3.prototype = Object.create(Parent3.prototype) // !!!核心(主要就是替換了這一句代碼) Child3.prototype.constructor = Child3const c1 = new Child3() console.log(c1.son) console.log(c1.getName())它利用 Object.create() 創(chuàng)建了父類構(gòu)造函數(shù)原型的(副本)淺復(fù)制,并將其賦值給子類構(gòu)造函數(shù)的原型,這樣子類的實例對象就可以訪問到父類的原型屬性和方法,同時它不會額外調(diào)用一次父類。
同樣的,根據(jù)在原型式繼承里的探究,這句核心代碼也可以用如下代碼代替(當然,僅僅作一個探討):
Child3.prototype.__proto__ = Parent3.prototype //或者 Object.setPrototypeOf(Child3.prototype, Parent3.prototype)寄生組合繼承模式是目前最優(yōu)的繼承方式,其實ES6的extends的語法糖本質(zhì)上也正與這種繼承方式基本類似。
以上六種繼承方式的簡單總結(jié):
7. ES6 - extends
直接一段代碼結(jié)束吧:
class Car {constructor(width,speed) {this.width = widththis.speed = speed} } class Truck extends Car {constructor(width,speed){super(width,speed)this.width = 7 // 覆蓋this.Container = true //重寫} } const truck = new Truck(6,40) console.log(truck); // Truck { width: 7, speed: 40, Container: true }參考文章:
面試官:Javascript如何實現(xiàn)繼承?
寄生組合式繼承
原型方法,沒有 proto 的對象
總結(jié)
以上是生活随笔為你收集整理的JavaScript六种继承方式的递进推演的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 解决外部工具链接CRM失败的问题
- 下一篇: html比较难记的点