首頁 >web前端 >js教程 >再談javascript原型繼承_javascript技巧

再談javascript原型繼承_javascript技巧

WBOY
WBOY原創
2016-05-16 16:31:451227瀏覽

真正意義上來說Javascript並不是一門物件導向的語言,沒有提供傳統的繼承方式,但是它提供了一種原型繼承的方式,利用自身提供的原型屬性來實現繼承。

原型與原型鏈

說原型繼承之前還是要先說說原型和原型鏈,畢竟這是實現原型繼承的基礎。
在Javascript中,每個函數都有一個原型屬性prototype指向自身的原型,而由這個函數創建的對像也有一個__proto__屬性指向這個原型,而函數的原型是一個對象,所以這個對像也會有一個__proto__指向自己的原型,這樣逐層深入到Object物件的原型,這樣就形成了原型鏈。下面這張圖很好的解釋了Javascript中的原型和原型鏈的關係。

每個函數都是Function函數建立的對象,所以每個函數也有一個__proto__屬性指向Function函數的原型。這裡要指出的是,真正形成原型鏈的是每個物件的__proto__屬性,而不是函數的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

這種是最簡單實作原型繼承的方法,直接把父類別的物件賦值給子類別建構子的原型,這樣子類別的物件就可以存取到父類別以及父類別建構子的prototype中的屬性。 此方法的原型繼承圖如下:

這種方法的優點很明顯,實現十分簡單,不需要任何特殊的操作;同時缺點也很明顯,如果子類需要做跟父類構造函數中相同的初始化動作,那麼就得在子類建構函式中再重複一次父類別中的操作:

複製程式碼 程式碼如下:

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屬性,如果初始化工作不斷增加,這種方式是很不方便的。因此就有了下面一種改進的方式。

借用建構子

複製程式碼 程式碼如下:

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

上面這種方法在子類別建構函式中透過apply呼叫父類別的建構函式來進行相同的初始化工作,這樣不管父類別中做了多少初始化工作,子類別也可以執行同樣的初始化工作。但是上面這種實作還存在一個問題,父類別建構函式被執行了兩次,一次是在子類別建構函式中,一次在賦值子類別原型時,這是很多餘的,所以我們還需要做一個改進:

複製程式碼 程式碼如下:

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

這樣我們就只需要在子類別建構子中執行一次父類別的建構函數,同時又可以繼承父類別原型中的屬性,這也比較符合原型的初衷,就是把需要重複使用的內容放在原型中,我們也只是繼承了原型中可重複使用的內容。上面這種方式的原型圖如下:

臨時建構子模式(聖杯模式)

上面借用構造函數模式最後改進的版本還是存在問題,它把父類的原型直接賦值給子類的原型,這就會造成一個問題,就是如果對子類的原型做了修改,那麼這個修改同時也會影響父類的原型,進而影響父類對象,這肯定不是大家所希望看到的。為了解決這個問題就有了臨時建構函數模式。

複製程式碼 程式碼如下:

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

此方法的原型繼承圖如下:

很容易可以看出,透過在父類原型和子類原型之間加入一個臨時的構造函數F,切斷了子類原型和父類原型之間的聯繫,這樣當子類原型做修改時就不會影響到父類原型。

我的方法

《Javascript模式》中到聖杯模式就結束了,可是不管上面哪一種方法都有一個不容易被發現的問題。大家可以看到我在'Parent'的prototype屬性中加入了一個obj物件字面量屬性,但是一直都沒有用。我們在聖杯模式的基礎上來看看下面這種情況:

複製程式碼 程式碼如下:

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

在上面這種情況中,當我修改child物件obj.a的時候,同時父類別的原型中的obj.a也會被修改,這就發生了和共享原型同樣的問題。出現這個情況是因為當訪問child.obj.a的時候,我們會沿著原型鏈一直找到父類的prototype中,然後找到了obj屬性,然後對obj.a進行修改。再看看下面這種情況:

複製程式碼 程式碼如下:

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

這裡有一個關鍵的問題,當物件存取原型中的屬性時,原型中的屬性對於物件來說是唯讀的,也就是說child物件可以讀取obj對象,但無法修改原型中obj對象引用,所以當child修改obj的時候並不會對原型中的obj產生影響,它只是在自身物件添加了一個obj屬性,覆蓋了父類別原型中的obj屬性。而當child物件修改obj.a時,它先讀取了原型中obj的引用,這時候child.obj和Parent.prototype.obj是指向同一個物件的,所以child對obj.a的修改會影響到Parent.prototype.obj.a的值,進而影響父類別的物件。 AngularJS中關於$scope嵌套的繼承方式就是模範Javasript中的原型繼承來實現的。
根據上面的描述,只要子類別對像中存取到的原型跟父類原型是同一個對象,那麼就會出現上面這種情況,所以我們可以對父類原型進行拷貝然後再賦值給子類原型,這樣當子類別修改原型中的屬性時就只是修改父類別原型的一個拷貝,並不會影響到父類別原型。具體實現如下:

複製程式碼 程式碼如下:

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繼承的具體實作如下,這裡只考慮了Child和Parent都是函數的情況下:

複製程式碼 程式碼如下:

var deepClone = 函數(源,目標){
    來源=來源|| {} ;
    var toStr = Object.prototype.toString ,
        arrStr = '[物件陣列]';
    for(原始碼中的 var i){
        if(source.hasOwnProperty(i)){
            var item = source[i] ;
            if(typeof item === 'object'){
                target[i] = (toStr.apply(item).toLowerCase() === arrStr) : [] ? {} ;
                deepClone(專案,目標[i]);   
            }其他{
                deepClone(item,target[i]) ;
            }
        }
    }
    返回目標;
} ;

varextend = function(Parent,Child){
    孩子=孩子||函數(){};
    if(父 === 未定義)
        返回子女;
    //借用父類別建構子
    子=函數(){
        Parent.apply(this,argument) ;
    } ;
    //透過拷貝深度繼承父類原型   
    Child.prototype = deepClone(Parent.prototype) ;
    //重置建構子屬性
    Child.prototype.constructor = Child ;
} ;

總結

說了這麼多,其實Javascript中實作繼承是十分靈活多樣的,並沒有一種最好的方法,需要根據不同的需求實現不同方式的繼承,最重要的是要了解Javascript中實現繼承的原理,原型和原型鏈的問題,只要理解了這些,自己實現繼承就可以游刃有餘。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn