js原型繼承

在javascript中,每個函數(shù)都有一個原型屬性prototype指向函數(shù)自身的原型,而由這個函數(shù)創(chuàng)建的對象也有一個proto屬性指向這個原型,而函數(shù)的原型是一個對象,所以這個對象也會有一個proto指向自己的原型,這樣逐層深入知道Object對象的原型(null),就形成了原型鏈。

圖片.png

每個函數(shù)都是Function函數(shù)創(chuàng)建的對象,所以每個函數(shù)也有一個proto屬性指向Function函數(shù)的原型。這里需要指正的是,真正形成原型鏈的是每個對象的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

這種方法的原型繼承圖如下:

圖片.png

這種方法優(yōu)點很明顯,實現(xiàn)十分簡單,不需要任何特殊的操作;同時缺點也很明顯,如果子類需要做跟父類構(gòu)造函數(shù)中相同的初始化動作,那么就得在子類構(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í)行同樣的初始化工作。但是上面這種實現(xiàn)還存在一個問題,父類構(gòu)造函數(shù)被執(zhí)行了兩次,一次是在子類構(gòu)造函數(shù)中,一次是在賦值子類原型時,這是很多余的,我們還需要一次改進(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ù),同時又可以繼承父類原型中的屬性,這比較符合原型的初衷,就是需要復(fù)用的內(nèi)容放在原型中,我們也只是繼承了原型中可復(fù)用的內(nèi)容。上面這種方式的原型圖如下:

圖片.png

臨時構(gòu)造函數(shù)模式(圣杯模式)

上面借用構(gòu)造函數(shù)模式最后改進(jìn)的版本還是存在問題,它把父類的原型直接賦值給子類的原型,就會造成一個問題,就是如果對子類的原型做了修改,那么這個修改同時也會影響到父類的原型,進(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);
}

var F = 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
圖片.png

很容易可以看出,通過在父類原型和子類原型之間加入一個臨時的構(gòu)造函數(shù)F,切斷了子類原型和父類原型之間的聯(lián)系,這樣,當(dāng)子類原型做修改時就不會影響到父類原型。

大神的做法

《javascript模式》中圣杯模式就結(jié)束了,可是不管上面哪種方法都有一個不容易被發(fā)現(xiàn)的問題。大家可以看到我在Parent的prototype屬性中加入了一個obj對象字面量屬性,但是一直沒有用,我們在圣杯模式的基礎(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 = 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對象obj.a的時候,同時父類原型中的obj.a也會被修改,這就發(fā)生了和共享原型同樣的問題。出現(xiàn)這個情況是因為當(dāng)訪問child.obj.a的時候,我們會沿著原型鏈一直找到父類的prototype中,然后找到了obj屬性,然后對obj.a進(jìn)行修改。
這里有一個關(guān)鍵的問題,當(dāng)對象訪問原型中的屬性時,原型中的屬性對于對象來說是只讀的,也就是說child對象可以讀取obj對象,但是無法修改原型中obj對象引用,所以當(dāng)child修改obj的時候并不會對原型中的obj產(chǎn)生影響,它只是在自身對象添加了一個obj屬性,覆蓋了父類原型中的obj屬性。而當(dāng)child對象修改obj.a時,它先讀取了原型中obj的引用,這時候child.obj和Parent.prototype.obj是指向同一個對象的,所以child對obj.a的修改會影響到Parent.prototype.obj.a的值,進(jìn)而影響到父類的對象。AngularJS中關(guān)于$scope嵌套的繼承方式就是模仿Javascript中的原型繼承來實現(xiàn)的。
根據(jù)上面的描述,只要子類對象中訪問的原型跟父類原型是同一個對象,那么就會出現(xiàn)上面這種情況,所以我們可以對父類原型進(jìn)行拷貝然后再賦值給子類原型,這樣當(dāng)子類修改原型中的屬性時就是修改父類原型的一個拷貝,并不會影響父類原型,具體實現(xiàn)如下:

var deepClone = function(source,target){
    source = source || {} ;
    target = target || {};
    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{
                target[i] = item;
            }
        }
    }
    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繼承的具體實現(xiàn)如下,這里只考慮了Child和Parent都是函數(shù)的情況下:

var deepClone = function(source,target){
    source = source || {} ;
    target = target || {};
    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{
                target[i] = item;
            }
        }
    }
    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 ;
} ;

參考自:https://segmentfault.com/a/1190000000766541

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容