再談javascript原型繼承
真正意義上來說Javascript并不是一門面向?qū)ο蟮恼Z言,沒有提供傳統(tǒng)的繼承方式,但是它提供了一種原型繼承的方式,利用自身提供的原型屬性來實(shí)現(xiàn)繼承。
原型與原型鏈
說原型繼承之前還是要先說說原型和原型鏈,畢竟這是實(shí)現(xiàn)原型繼承的基礎(chǔ)。
在Javascript中,每個(gè)函數(shù)都有一個(gè)原型屬性prototype指向自身的原型,而由這個(gè)函數(shù)創(chuàng)建的對(duì)象也有一個(gè)__proto__屬性指向這個(gè)原型,而函數(shù)的原型是一個(gè)對(duì)象,所以這個(gè)對(duì)象也會(huì)有一個(gè)__proto__指向自己的原型,這樣逐層深入直到Object對(duì)象的原型,這樣就形成了原型鏈。下面這張圖很好的解釋了Javascript中的原型和原型鏈的關(guān)系。
每個(gè)函數(shù)都是Function函數(shù)創(chuàng)建的對(duì)象,所以每個(gè)函數(shù)也有一個(gè)__proto__屬性指向Function函數(shù)的原型。這里需要指出的是,真正形成原型鏈的是每個(gè)對(duì)象的__proto__屬性,而不是函數(shù)的prototype屬性,這是很重要的。
原型繼承
基本模式
var Parent = function(){
this.name = 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(){
this.name = 'child' ;
} ;
Child.prototype = new Parent() ;
var parent = new Parent() ;
var child = new Child() ;
console.log(parent.getName()) ; //parent
console.log(child.getName()) ; //child
這種是最簡單實(shí)現(xiàn)原型繼承的方法,直接把父類的對(duì)象賦值給子類構(gòu)造函數(shù)的原型,這樣子類的對(duì)象就可以訪問到父類以及父類構(gòu)造函數(shù)的prototype中的屬性。 這種方法的原型繼承圖如下:
這種方法的優(yōu)點(diǎn)很明顯,實(shí)現(xiàn)十分簡單,不需要任何特殊的操作;同時(shí)缺點(diǎn)也很明顯,如果子類需要做跟父類構(gòu)造函數(shù)中相同的初始化動(dòng)作,那么就得在子類構(gòu)造函數(shù)中再重復(fù)一遍父類中的操作:
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
this.name = name || 'child' ;
} ;
Child.prototype = new Parent() ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild
上面這種情況還只是需要初始化name屬性,如果初始化工作不斷增加,這種方式是很不方便的。因此就有了下面一種改進(jìn)的方式。
借用構(gòu)造函數(shù)
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
Child.prototype = new Parent() ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild
上面這種方法在子類構(gòu)造函數(shù)中通過apply調(diào)用父類的構(gòu)造函數(shù)來進(jìn)行相同的初始化工作,這樣不管父類中做了多少初始化工作,子類也可以執(zhí)行同樣的初始化工作。但是上面這種實(shí)現(xiàn)還存在一個(gè)問題,父類構(gòu)造函數(shù)被執(zhí)行了兩次,一次是在子類構(gòu)造函數(shù)中,一次在賦值子類原型時(shí),這是很多余的,所以我們還需要做一個(gè)改進(jìn):
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
Child.prototype = Parent.prototype ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild
這樣我們就只需要在子類構(gòu)造函數(shù)中執(zhí)行一次父類的構(gòu)造函數(shù),同時(shí)又可以繼承父類原型中的屬性,這也比較符合原型的初衷,就是把需要復(fù)用的內(nèi)容放在原型中,我們也只是繼承了原型中可復(fù)用的內(nèi)容。上面這種方式的原型圖如下:
臨時(shí)構(gòu)造函數(shù)模式(圣杯模式)
上面借用構(gòu)造函數(shù)模式最后改進(jìn)的版本還是存在問題,它把父類的原型直接賦值給子類的原型,這就會(huì)造成一個(gè)問題,就是如果對(duì)子類的原型做了修改,那么這個(gè)修改同時(shí)也會(huì)影響到父類的原型,進(jìn)而影響父類對(duì)象,這個(gè)肯定不是大家所希望看到的。為了解決這個(gè)問題就有了臨時(shí)構(gòu)造函數(shù)模式。
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
var F = new Function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(parent.getName()) ; //myParent
console.log(child.getName()) ; //myChild
該方法的原型繼承圖如下:
很容易可以看出,通過在父類原型和子類原型之間加入一個(gè)臨時(shí)的構(gòu)造函數(shù)F,切斷了子類原型和父類原型之間的聯(lián)系,這樣當(dāng)子類原型做修改時(shí)就不會(huì)影響到父類原型。
我的方法
《Javascript模式》中到圣杯模式就結(jié)束了,可是不管上面哪一種方法都有一個(gè)不容易被發(fā)現(xiàn)的問題。大家可以看到我在'Parent'的prototype屬性中加入了一個(gè)obj對(duì)象字面量屬性,但是一直都沒有用。我們?cè)谑ケJ降幕A(chǔ)上來看看下面這種情況:
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
var F = new Function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = 2 ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //2
在上面這種情況中,當(dāng)我修改child對(duì)象obj.a的時(shí)候,同時(shí)父類的原型中的obj.a也會(huì)被修改,這就發(fā)生了和共享原型同樣的問題。出現(xiàn)這個(gè)情況是因?yàn)楫?dāng)訪問child.obj.a的時(shí)候,我們會(huì)沿著原型鏈一直找到父類的prototype中,然后找到了obj屬性,然后對(duì)obj.a進(jìn)行修改。再看看下面這種情況:
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : 1} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
var F = new Function(){} ;
F.prototype = Parent.prototype ;
Child.prototype = new F() ;
var parent = new Parent('myParent') ;
var child = new Child('myChild') ;
console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = 2 ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //2
這里有一個(gè)關(guān)鍵的問題,當(dāng)對(duì)象訪問原型中的屬性時(shí),原型中的屬性對(duì)于對(duì)象來說是只讀的,也就是說child對(duì)象可以讀取obj對(duì)象,但是無法修改原型中obj對(duì)象引用,所以當(dāng)child修改obj的時(shí)候并不會(huì)對(duì)原型中的obj產(chǎn)生影響,它只是在自身對(duì)象添加了一個(gè)obj屬性,覆蓋了父類原型中的obj屬性。而當(dāng)child對(duì)象修改obj.a時(shí),它先讀取了原型中obj的引用,這時(shí)候child.obj和Parent.prototype.obj是指向同一個(gè)對(duì)象的,所以child對(duì)obj.a的修改會(huì)影響到Parent.prototype.obj.a的值,進(jìn)而影響父類的對(duì)象。AngularJS中關(guān)于$scope嵌套的繼承方式就是模范Javasript中的原型繼承來實(shí)現(xiàn)的。
根據(jù)上面的描述,只要子類對(duì)象中訪問到的原型跟父類原型是同一個(gè)對(duì)象,那么就會(huì)出現(xiàn)上面這種情況,所以我們可以對(duì)父類原型進(jìn)行拷貝然后再賦值給子類原型,這樣當(dāng)子類修改原型中的屬性時(shí)就只是修改父類原型的一個(gè)拷貝,并不會(huì)影響到父類原型。具體實(shí)現(xiàn)如下:
var deepClone = function(source,target){
source = source || {} ;
var toStr = Object.prototype.toString ,
arrStr = '[object array]' ;
for(var i in source){
if(source.hasOwnProperty(i)){
var item = source[i] ;
if(typeof item === 'object'){
target[i] = (toStr.apply(item).toLowerCase() === arrStr) : [] ? {} ;
deepClone(item,target[i]) ;
}else{
deepClone(item,target[i]) ;
}
}
}
return target ;
} ;
var Parent = function(name){
this.name = name || 'parent' ;
} ;
Parent.prototype.getName = function(){
return this.name ;
} ;
Parent.prototype.obj = {a : '1'} ;
var Child = function(name){
Parent.apply(this,arguments) ;
} ;
Child.prototype = deepClone(Parent.prototype) ;
var child = new Child('child') ;
var parent = new Parent('parent') ;
console.log(child.obj.a) ; //1
console.log(parent.obj.a) ; //1
child.obj.a = '2' ;
console.log(child.obj.a) ; //2
console.log(parent.obj.a) ; //1
綜合上面所有的考慮,Javascript繼承的具體實(shí)現(xiàn)如下,這里只考慮了Child和Parent都是函數(shù)的情況下:
var deepClone = function(source,target){
source = source || {} ;
var toStr = Object.prototype.toString ,
arrStr = '[object array]' ;
for(var i in source){
if(source.hasOwnProperty(i)){
var item = source[i] ;
if(typeof item === 'object'){
target[i] = (toStr.apply(item).toLowerCase() === arrStr) : [] ? {} ;
deepClone(item,target[i]) ;
}else{
deepClone(item,target[i]) ;
}
}
}
return target ;
} ;
var extend = function(Parent,Child){
Child = Child || function(){} ;
if(Parent === undefined)
return Child ;
//借用父類構(gòu)造函數(shù)
Child = function(){
Parent.apply(this,argument) ;
} ;
//通過深拷貝繼承父類原型
Child.prototype = deepClone(Parent.prototype) ;
//重置constructor屬性
Child.prototype.constructor = Child ;
} ;
總結(jié)
說了這么多,其實(shí)Javascript中實(shí)現(xiàn)繼承是十分靈活多樣的,并沒有一種最好的方法,需要根據(jù)不同的需求實(shí)現(xiàn)不同方式的繼承,最重要的是要理解Javascript中實(shí)現(xiàn)繼承的原理,也就是原型和原型鏈的問題,只要理解了這些,自己實(shí)現(xiàn)繼承就可以游刃有余。
相關(guān)文章
JavaScript獲取XML數(shù)據(jù)附示例截圖
這篇文章主要介紹了JavaScript獲取XML數(shù)據(jù)的方法,需要的朋友可以參考下2014-03-03window.open()實(shí)現(xiàn)post傳遞參數(shù)
本文主要向大家介紹了如何使用window.open()實(shí)現(xiàn)post傳遞參數(shù)的方法,思路是參考的一位網(wǎng)友的,然后根據(jù)自己的項(xiàng)目需求做了些調(diào)整,這里同樣分享給大家,希望對(duì)大家能夠有所幫助。2015-03-03微信小程序自定義菜單切換欄tabbar組件代碼實(shí)例
這篇文章主要介紹了微信小程序自定義菜單切換欄tabbar組件代碼實(shí)例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12詳解babel是如何將class語法糖轉(zhuǎn)換為es5的語法
這篇文章主要詳細(xì)介紹了babel是如何將class語法糖轉(zhuǎn)換為es5的語法,文中通過代碼示例給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作有一定的幫助,需要的朋友可以參考下2024-02-02如何消除inline-block屬性帶來的標(biāo)簽間間隙
這篇文章主要介紹了如何消除inline-block屬性帶來的標(biāo)簽間間隙的相關(guān)資料,需要的朋友可以參考下2016-03-03D3.js實(shí)現(xiàn)力向?qū)D的繪制教程詳解
力向?qū)D是繪圖的一種算法,實(shí)現(xiàn)了用以模擬粒子物理運(yùn)動(dòng)的?velocity?Verlet?數(shù)值積分器。本文將利用D3.js實(shí)現(xiàn)力向?qū)D的繪制,需要的可以參考一下2022-11-11