JavaScript 常見的繼承方式彙總
原型鏈機制:
在ECMAscript中描述了原型鏈的概念,並將原型鏈作為實現繼承的主要方法,其基本思想就是利用原型讓一個引用型別繼承另一個引用型別的屬性和方法。
建構函式和原型還有例項之間的關係:
每個建構函式都有一個原型物件(prototype),原型物件都包含一個指向建構函式的指標(constructor),而例項都包含一個指向原型物件的內部指標 ( __propto__ ) 。關係圖如下圖所示:
每一個Function都是Object基類的一個例項,所以每一個Function上都有一個__proto__指向了Object.prototype。
當查詢一個例項的屬性時,會先從這個例項的自定義屬性上找,如果沒有的話通過__proto__去例項所屬類的原型上去找,如果還沒有的話再通過原型(原型也是物件,只要是物件就有__proto__屬性)的__proto__到Object的原型上去找,一級一級的找,如果沒有就undefined。
所以引用型別之間的繼承就是通過原型鏈機制實現的。
一.原型繼承
原型繼承:把父類的私有+公有的屬性和方法,都作為子類公有的屬性。
核心:不是把父類私有+公有的屬性克隆一份一模一樣的給子類的公有。他是通過__proto__建立和子類之間的原型鏈,當子類的例項需要使用父類的屬性和方法的時候,可以通過__proto__一級級找上去使用。
function Parent(){ this.x = 199; this.y = 299; } Parent.prototype.say = function(){ console.log('say') } function Child(){ this.g = 90; } Child.prototype = new Parent(); var p = new Parent(); var c = new Child(); console.dir(c)
實現的本質是重寫了原型物件 ,通過將子類的原型指向了父類的例項,所以子類的例項就可以通過__proto__訪問到 Child.prototype 也就是 Parent的例項,這樣就可以訪問到父類的私有方法。然後再通過__proto__指向父類的prototype就可以獲得到父類原型上的方法。
這樣就做到了將父類的私有、公有方法和屬性都當做子類的公有屬性。這樣就通過原型鏈實現了繼承。
但是別忘了預設的原型,因為所有引用型別都是繼承了Object的,所有說子類也可以訪問到Object上的方法如toString() 、valueOf() 等。
結果如下圖所示:
有的時候我們需要在子類中新增新的方法或者是重寫父類的方法時候,切記一定要放到替換原型的語句之後
function Parent(){ this.x = 199; this.y = 299; } Parent.prototype.say = function(){ console.log('say') } function Child(){ this.g = 90; } /*Child.prototype.Bs = function(){ console.log('Bs') }*/在這裡寫子類的原型方法和屬性是沒用的因為會改變原型的指向,所以應該放到重新指定之後 Child.prototype = new Parent(); Child.prototype.constructor=Child//由於重新修改了Child的原型導致預設原型上的constructor丟失,我們需要自己新增上,其實沒啥用,加不加都一樣 Child.prototype.Bs = function(){ console.log('Bs') } Child.prototype.say = function(){ console.log('之後改的') } var p = new Parent(); var c = new Child(); console.dir(c) c.Bs() //Bs c.say() // 之後改的 p.say() //say 不影響父類例項訪問父類的方法
存在的問題:
1. 子類繼承父類的屬性和方法是將父類的私有屬性和公有方法都作為自己的公有屬性和方法,我們要清楚一件事情就是我們操作基本資料型別的時候操作的是值,在操作應用資料型別的時候操作的是地址,如果說父類的私有屬性中引用型別的屬性,那他被子類繼承的時候會作為公有屬性,這樣子類一操作這個屬性的時候,會影響到子類二。
2. 在建立子類的例項時,不能向父型別的建構函式中傳遞引數。應該說是沒有辦法在不影響所有物件例項的情況下,給父類的建構函式傳遞引數。
所以在實際中很少單獨使用原型繼承。
二.call繼承
改變方法的this指向,同時執行方法。 在子類建構函式中父類.call(this) 可以將父類的私有變成子類的私有。
function Parent() { this.x = 100; this.y = 199; } Parent.prototype.fn = function() {} function Child() { this.d = 100; Parent.call(this); //建構函式中的this就是當前例項 } var p = new Parent(); var c = new Child(); console.log(p) //Parent {x: 100,y: 199} console.log(c) //Child {d: 100,x: 100,y: 199}
在子類的建構函式中,改變父類的this指向,改變為子類的例項,同時執行父類方法,這樣父類中的this.x就變成了子類的例項.x,通過這種方法就可以繼承了父類的私有屬性,且只能繼承父類的私有屬性和方法。
三.冒充物件繼承
冒充物件繼承的原理是迴圈遍歷父類例項,然後父類例項的私有方法全部拿過來新增給子類例項。
function Parent(){ this.x = 100; } Parent.prototype.getX = function(){ console.log('getX') } function Child(){ var p = new Parent(); for(var attr in p){//for in 可以遍歷到原型上的公有自定義屬性 this[attr] = p[attr] } //以下程式碼是隻獲得到私有方法和屬性,如果不加這個的話就可以遍歷到所有方法和屬性 /*if(e.hasOwnProperty(attr)){ this[attr] = e[attr] } e.propertyIsEnumerable()*///可列舉屬性==> 可以拿出來一一列舉的屬性 } var p = new Parent(); var c = new Child(); console.dir(c)
for in 可以遍歷到原型上的公有自定義屬性 ,所以他可以拿到私有和公有的屬性和方法,這個你可以遍歷私有和公有的,需要你加限制條件。但是如果不做hasOwnProperty判斷那麼就是把父類的公有的和私有的都拿過來當私有的。
四.混合繼承
就是將call繼承和原型繼承集合在一起,無論是私有的還是公有的都拿過來了。但是有個問題就是子類的原型上的多了一套父類私有屬性,但是不會產生問題。因為子類的私有屬性也有一套相同的通過call繼承拿過來的。
function Parent(){ this.x=100; } Parent.prototype.getX = function(){} function Child(){ Parent.call(this); } Child.prototype = new Parent(); Child.prototype.constructor = Child; var p = new Parent(); var c = new Child(); console.log(c)//Child {x: 100}
存在的問題:
無論在什麼情況下,都會呼叫兩次建構函式:一次是在建立子型別原型的時候,另一次是在子型別建構函式的內部,沒錯,子型別最終會包含父型別物件的全部例項屬性,但我們不得不在呼叫子類建構函式時重寫這些屬性。
還有一種就是call+拷貝繼承
//混合繼承:call繼承+拷貝繼承 function extend(newEle,oldEle){ for(var attr in oldEle){ newEle[attr]=oldEle[attr]; } } function F(){ this.x=100; this.showX=function(){} } F.prototype.getX=function(){}; F.prototype.getX1=function(){}; var f1=new F; console.dir(f1) function S(){ F.call(this)//call繼承 } extend(S.prototype,F.prototype);//拷貝繼承 S.prototype.cc=function(){ } var p1=new S; console.dir(p1);
這種方式使用call繼承將父類的私有方法繼承過來,使用for in 拷貝將父類的公有屬性和方法繼承過來,比較實用。
五.中介軟體繼承
中介軟體繼承就是通過原型鏈的機制,子類的prototype.__proto__本來應該是直接指向Object.prototype。
從父類的原型上的__proto__也可以到Object.prototype,在父類.prototype上停留了下,父類.prototype就是一箇中間件,所以子類可以繼承到父類的公有方法當做自己的公有方法。
function Parent(){ this.x = 100; } Parent.prototype.getX = function(){} function Child(){ } Child.prototype.__proto__ = Parent.prototype; var p = new Parent(); var c = new Child() console.log(c)
六.寄生組合式繼承
寄生式組合: call繼承+Object.create();
所謂寄生組合式繼承就是通過借用建構函式來繼承屬性,通過原型鏈的混合形式來繼承方法。
基本思路是不必為了指定子類的原型而呼叫父類的建構函式,我們所需要的就是父型別原型的一個副本。
本質上,就是使用寄生式繼承父類的原型,然後再將結果指定給子類的原型。
function F(){ this.x=100; } F.prototype.showX=function(){}; function S(){ this.y = 200 F.call(this)//只繼承了私有的; } function inheritPrototype(subType,superType){ var prototype = Object.create(superType.prototype);//建立物件 prototype.constructor = subType;//增強物件 subType.prototype = prototype;//指定物件 } inheritPrototype(S,F) var p1=new S; console.dir(p1)
1、第一步是建立父型別原型的一個副本。
2、第二步是為建立的副本增加constructor屬性,從而彌補了因為重寫原型而失去的預設的constructor屬性。
3、第三步是將建立的物件賦值給子型別的原型。
這個例子的高效率體現在他只調用了一次SuperType 建構函式,並且因此避免了在SubType.prototype上面建立不必要的、多餘的屬性。與此同時原型鏈還能保持不變,所以可以正常使用instanceof 和 isPrototypeOf() ,所以寄生組合繼承是引用型別最理想的繼承方法。
七.class繼承
class 可以通過extends關鍵字實現繼承,這比 ES5 的通過修改原型鏈實現繼承,要清晰和方便很多。
class Father{ constructor(x,y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ',' + this.y + ')'; } } class Son extends Father{ constructor(x,y,color){ super(x,y); // 呼叫父類的constructor(x,y) this.color = color; } toString() { console.log( super.toString()+this.color); // 呼叫父類的toString() } } let son = new Son(3,4,'red'); son.toString();//結果為(3,4)red
上面程式碼定義了一個Son類,該類通過extends關鍵字,繼承了Father類的所有屬性和方法。
上面程式碼中,constructor方法和toString方法之中,都出現了super關鍵字,它在這裡表示父類的建構函式,用來新建父類的this物件。
子類必須在constructor方法中呼叫super方法,否則新建例項時會報錯。這是因為子類自己的this物件,必須先通過父類的建構函式完成塑造,得到與父類同樣的例項屬性和方法,然後再對其進行加工,加上子類自己的例項屬性和方法。如果不呼叫super方法,子類就得不到this物件。
以上就是JavaScript 常見的繼承方式彙總的詳細內容,更多關於JavaScript 繼承方式的資料請關注我們其它相關文章!