javascript繼承


JavsScript中對象繼承關系變得無關緊要,對於一個對象來說重要的是它能做什么,而不是它從哪里來。

JavaScript提供了一套更為豐富的代碼重用模式。它可以模擬那些基於類的模式,同時它也可以支持其他更具表現力的模式。

JavaScript是一門基於原型的語言,這意味着對象直接從其他對象繼承。

一、偽類

1、原理

javascript原型機制:不直接讓對象從其他對象繼承,反而插入了一個多余的間接層:通過構造器函數產生對象。

當一個函數對象被創建時,Function構造器產生的函數對象會運行類型這樣一些代碼:

this.prototype={constructor:this}

新函數對象被賦予一個prototype屬性,它的值是一個包含constructor屬性且屬性值為該新函數的對象。這個prototype對象是存放繼承特征的地方。

當采用構造器調用模式,即用new前綴去調用一個函數時,函數執行的方式會被修改。如果new運算符是一個方法而不是一個運算符,它可能會像這樣執行:

Function.method('new',function () {
    //創建一新對象,它繼承自構造器函數的原型對象。
    var that=Object.create(this.prototype);
    //調用構造器函數,綁定-this-到新對象上。
    var other=this.apply(that,arguments);
    //如果它的返回值不是一個對象,就返回該對象。
    return (typeof other==='object'&&other)||that;
});

2、偽類,即使用new前綴

定義一構造器並擴充它的原型:

var Mammal=function(name){
    this.name=name;
}
Mammal.prototype.get_name=function(){
    return this.name;
}
Mammal.prototype.says=function(){
    return this.saying || '';
}

現在構造一個實例:

var myMammal=new Mammal('Herb the Mammal');
var name=myMammal.get_name();//"Herb the Mammal"

構造另一個偽類繼承Mamal,這是通過定義它的constructor函數並替換它的prototype為一個Mammal的實例來實現的。

var Cat=function(name){
    this.name=name; //重復實現了一遍
    this.saying='meow';
}
//替換cat.prototype為一個新的Mammal實例
Cat.prototype=new Mammal();
//擴充新原型對象,增加purr和get_name方法。
Cat.prototype.purr=function(n){
    var i,s='';
    for(i=0;i<n;i+=1){
        if(s){
            s+='-'
        }
        s+='r';
    }
    return s;
}
Cat.prototype.get_name=function(){
    return this.says()+' '+this.name+' '+this.says();
}

var myCat=new Cat('Henrietta');
var says=myCat.says();//"meow"
var purr=myCat.purr(5);//"r-r-r-r-r"
var name=myCat.get_name();//"meow Henrietta meow"

偽類模式本意是想向面向對象靠攏,但它看起來格格不入。

我們隱藏一些丑陋的細節,通過使用method方法來定義一個inherits方法實現。

Function.prototype.method=function(name,func){
    if(!this.prototype[name]){
        this.prototype[name]=func;
    }
    return this;
}

Function.method('inherits',function(Parent){
    this.prototype=new Parent();
    return this;
});

var Cat=function(name){
    this.name=name;
    this.saying='meow'
}
.inherits(Mammal)
.method('purr',function(n){
    var i,s='';
    for(i=0;i<n;i+=1){
        if(s){
            s+='-'
        }
        s+='r';
    }
    return s;
})
.method('get_name',function(){
    return this.says()+' '+this.name+' '+this.says();
});

var myCat=new Cat('Henrietta');
var says=myCat.says();//"meow"
var purr=myCat.purr(5);//"r-r-r-r-r"
var name=myCat.get_name();//"meow Henrietta meow"

問題:以上雖然隱藏了prototype操作細節,但是問題還在:有了像“類” 的構造器函數,但仔細看它們,你會驚訝地發現:

1、沒有私有環境,所有的屬性都是公開的。

2、使用構造器函數存在一個嚴重的危害。如果調用構造函數時忘記了在前面加上new前綴,那么this將不會被綁定到一個新對象上。悲劇的是,this將被綁定到全局對象上,所以你不但沒有擴充新對象,反而破壞了全局變量環境。

這是一個嚴重的語言設計錯誤。為了降低這個問題帶來的風險,所有的構造器函數都約定命名成首字母大寫的形式,並且不以首字母大寫的形式拼寫任何其他的東西。

一個更好的備選方案就是根本不使用new。

二、對象說明符

構造器要接受一大串參數,要記住參數的順序非常困難。所以編寫構造器時讓它接受一個簡單的對象說明符,更友好。

//接受一大串參數
var myObject=maker(f,l,m,c,s);

//對象字面量更友好
var myObject=maker({
    first:f,
    middle:m,
    last:l,
    state:s,
    city:c
});

對象字面量好處:

  • 多個參數可以按任何順序排列
  • 構造器如果聰明的使用了默認值,一些參數可以忽略掉
  • 和JSON一起用時,可以把JSON對象傳給構造器,返回一個構造完全的對象

三、原型

原型模式中,摒棄類,轉而專注於對象。概念:一個新對象可以繼承一個舊對象的屬性。 通過構造一個有用的對象開始,接着可以構造更多和那個對象類似的對象。這就可以完全避免把一個應用拆解成一系列嵌套抽象類的分類過程。

1、差異化繼承

用對象字面量構造一個有用的對象。

var myMammal={
    name:"Herb the Mammal",
    get_name:function(){
        return this.name;
    },
    says:function(){
        return this.saying || '';
    }
}

一旦有了想要的對象,就可以利用Object.create方法構造出更多的實例。

var myCat=Object.create(myMammal);
myCat.name='Henrietta';
myCat.saying='meow';
myCat.purr=function(n){
    var i,s='';
    for(i=0;i<n;i++){
        if(s){
            s+='-';
        }
        s+='r';
    }
    return s;
}
myCat.get_name=function(){
    return this.says+' '+this.name+' '+this.says;
}

這是一種“差異化繼承(differential inheritance)”,通過定制一新的對象,我們指明它與所基於的基本對象的區別。

2、差異化繼承優勢

差異化繼承,對某些數據結構繼承於其他數據結構的情形非常有用。

例:假定我們要解析一門類似JavaScript這樣用一對花括號指示作用域的語言。定義在某個作用域里的條目在該作用域外是不可見的。

但在某種意義上,一個內部作用域會繼承它的外部作用域。JavaScript在表示這樣的關系上做得非常好。

當遇到一個左花括號時block函數被調用,parse函數將從scope中尋找符號,並且它定義了新的符號時擴充scope。

var block=function(){
    //記住當前的作用域。構造一包含了當前作用域中所有對象的新的作用域
    var oldScope=scope;
    scope=Object.create(scope);
    //傳遞左花括號作為參數調用advance
    advance('{');
    //使用新的作用域進行解析
    parse(scope);
    //傳遞右花括號作為參數調用advance並拋棄新作用域,恢復原來老的作用域
    advance('}');
    scope=oldScope;
}

四、函數化

至此,上面我們看到的繼承模式的一個弱點就是:沒法包含隱私。對象的所有屬性都是可見的。

應用模塊模式,可以解決這個問題。

1、模塊模式

從構造一個生成對象的函數開始。我們以小寫字母開頭來命名它,因為它並不需要使用new前綴。該函數包括4個步驟:

  1. 創建一個新對象。有很多的方式去構造一個對象
    • 對象字面量構造
    • new調用一個構造器函數
    • Object.create方法構造一個已經盡的對象的新實例
    • 調用任意一個會返回一個對象的函數
  2. 有選擇性地定義私有實例變量和方法。這些就是函數中通過var 語句定義的普通變量。
  3. 給這個新對象擴充方法。這些方法擁有特權去訪問參數以及在第二步中通過var語句定義的變量
  4. 返回那個新對象。

下面是一個函數化構造器的偽代碼模板(加粗的文本表示強調):

var constructor =function(spec,my){
    var that,其他私有實例變量;
    my=my||{};
    把共享的變量和函數添加到my中
    that=一個新對象
    添加給that的特權方法 return that;
}

說明:

spec對象包含構造器需要構造一新實例的所有信息。spec的內容可能被復制到私有變量中,或者被其他函數改變,或者方法可以在需要的時候訪問spec的信息。(一個簡化的方式是替換spec為一個單一的值。當構造對象過程匯總並不需要整個spec對象的時候,這是有用的)

my對象是一個為繼承鏈中的構造器提供秘密共享的容器。 my對象可以選擇性地使用。如果沒有傳入一個my對象,那么會創建一個my對象。

接下來,聲明該對象私有的實例變量和方法。 通過簡單的聲明變量就可以做到。構造器的變量和內部函數變成了該實例的私有成員。內部函數可以訪問spec,my,that,以及其他私有變量。

接下來,給my變量添加共享的秘密成員。這是通過賦值語句來實現的:

my.member=value;

現在,我們構造了一個新對象並把它賦值給that。構造新對象可能是通過調用函數化構造器,傳給它一個spec對象(可能就是傳遞給當前構造器的同一個spec對象)和my對象。my對象允許其他的構造器分享我們放到my中的資料。其他的構造器可能也會把自己可分享的秘密成員放進my對象里,以便我們的構造器可以利用它。

接下來,擴充that,加入組成該對象接口的特權方法。我們可以分配一個新函數稱為that的成員方法。或者,更安全地,我們可以先把函數定義為私有方法,然后再把它們分配給that

var methodical=function(){

...

};

that.methodical=methodical;

/*分開兩步去定義methodical的好處是,如果其他方法想要調用methodical,它們可以直接調用methodical()而不是that.methodical()。
如果該實例被破壞或篡改,甚至that.methodical被替換掉了,
調用methodical的方法同樣會繼續工作,因為它們私有的methodical不受該實例被修改的影響。*/

2、應用

我們把這個模式應用到mammal例子里。此處不需要my,所以我們先拋開它,但會使用一個spec對象。

var mammal=function(spec){
    var that={};
    that.get_name=function(){
        return spec.name;
    };
    that.says=function(){
        return spec.saying || '';
    }
    return that;
}

var myMammal=mammal({name:'Herb'});

 此時name就是私有屬性,被保護起來了。

 

在偽類模式里,構造器函數Cat不得不重復構造器Mammal已經完成的工作。在函數化模式中那不再重要了,因為構造器Cat將會調用構造器Mammal,讓Mammal去做對象創建中的大部分工作,所以Cat只需關注自身的差異即可。

var cat=function(spec){
    spec.saying=spec.saying || 'meow';
    var that=mammal(spec);
    that.purr=function(n){
        var i,s='';
        for(i=0;i<n;i++){
            if(s){
                s+='-';
            }
            s+='r';
        }
        return s;
    };
    that.get_name=function(){
        return that.says()+' '+spec.name+' '+that.says();
    };
    return that;
}

var myCat=cat({name:'Henrietta'});

 函數化模式還給我們提供了一個處理父類方法的方法。

我們會構造一個superior方法,它取得一個方法名並返回調用那個方法的函數。該函數會調用原來的方法,盡管屬性已經變化了。

/*有點難理解*/

Object.method('superior',function(name){ //傳入方法名name
    var that=this,method=that[name]; 
    return function(){
        return method.apply(that,argumetns);
    }
});

把調用superior應用在coolcat上, coolcat就像cat一樣,除了它有一個更酷的調用父類cat的方法的get_name方法。

它只需要一點點准備工作。我們會聲明一個super_get_name變量,並且把調用superior方法所返回的結果賦值給它。

var coolcat=function(spec){  //coolcat有一個更酷的調用父類cat的方法的get_name方法
    var that=cat(spec);
    var super_get_name=that.superior('get_name');
    that.get_name=function(n){
        return 'like '+super_get_name()+'baby';
    }
    return that;
}

var myCoolCat=coolcat({name:'Bix'});
var name=myCoolCat.get_name();//"like meow Bix meowbaby"

函數模塊化有很大的靈活性。它相比偽類模式不僅帶來的工作更少,還讓我們得到更好的封裝和信息隱藏,以及訪問父類方法的能力。

/*有點難理解*/

如果對象的所有狀態都是私有的,那么該對象就稱為了一個“防偽(tamper-proof)對象” 。該對象的屬性可以被替換或刪除,但該對象的完整性不會受到損害。

如果我們用函數化的模式創建一個對象,並且該對象的所有方法都不使用this或that,那么該對象就是持久性(durable)的。

一個持久性的對象不會被入侵。訪問一個持久性的對象時,除非有方法授權,否則攻擊者不能訪問對象的內部狀態。

總結一下以上整個完美的繼承鏈的代碼:

<script>
/* *****mammal object***** */
var mammal=function(spec){
    var that={};
    that.get_name=function(){
        return spec.name;
    };
    that.says=function(){
        return spec.saying || '';
    }
    return that;
}
//call
var myMammal=mammal({name:'Herb'});

/* *****cat object***** */
var cat=function(spec){
    spec.saying=spec.saying || 'meow';
    var that=mammal(spec);
    that.purr=function(n){
        var i,s='';
        for(i=0;i<n;i++){
            if(s){
                s+='-';
            }
            s+='r';
        }
        return s;
    };
    that.get_name=function(){
        return that.says()+' '+spec.name+' '+that.says();
    };
    return that;
}
//call
var myCat=cat({name:'Henrietta'});

/*user-defined Method*/
Function.prototype.method=function(name,func){
    if(!this.prototype[name]){
        this.prototype[name]=func;
    }
    return this;
}

Object.method('superior',function(name){ //傳入方法名name
    var that=this,method=that[name]; 
    return function(){
        return method.apply(that,arguments);
    }
});

/* *****coolcat object***** */
var coolcat=function(spec){  //coolcat有一個更酷的調用父類cat的方法的get_name方法
    var that=cat(spec);
    var super_get_name=that.superior('get_name');
    that.get_name=function(n){
        return 'like '+super_get_name()+'baby';
    }
    return that;
}
//call
var myCoolCat=coolcat({name:'Bix'});
var name=myCoolCat.get_name();//"like meow Bix meowbaby"
</script>
View Code

五、部件(Parts)

我們可以從一套部件中把對象組裝出來。

例如,我們可以構造一個給任何對象添加簡單事件處理特性的函數。它會給對象添加一個on方法,一個fire方法和一個私有的事件注冊表對象:

<script>
var eventuality=function(that){
    var registry={};  //注冊表
    that.fire=function(event){
        //在一個對象上觸發一個事件。該事件可以是一個包含事件名稱的字符串,
        //或者是一個擁有包含事件名稱的type屬性的對象。
        //通過'on'方法注冊的事件處理程序中匹配事件名稱的函數將被調用
        var array,
              func,
              handler,
              i,
              type=typeof event ==='string'?event:event.type;
         //如果這個事件存在一組事件處理程序,那么就遍歷它們並按順序依次執行。
         if(registry.hasOwnProperty(type))     {
            array=registry[type];
            for(i=0;i<array.length;i++){
                handler=array[i];
                //每個處理程序包含一個方法和一組可選的參數。
                //如果該方法是一個字符串形式的名稱,那么尋找到該函數。
                func=handler.method;
                if(typeof func==='string'){
                    func=this[func];
                }
                //調用一個處理程序。如果該條目包含參數,那么傳遞它們過去。否則,傳遞該事件對象。
                func.apply(this,handler.paramenters || [event]);
            }
         }
         return this;
    }

    that.on=function(type,method,parameters){
        //注冊一個事件。構造一條處理程序條目。將它插入到處理程序數組中,
        //如果這種類型的事件還不存在,就構造一個。
        var handler={
            method:method,
            parameters:parameters
        };
        if(registry.hasOwnProperty(type)){
            registry[type].push(handler);
        }else{
            registry[type]=[handler];
        }
        return this;
    }
    return that;
}
</script>

我們可以在任何單獨的對象上調用eventuality,授予它事件處理方法。 我們也可以趕在that被返回前在一個構造器函數中調用它。eventlity(that);

用這種方式,一個構造器函數可以從一套布局中把對象組裝出來。JavaScript的弱類型在此處是一個巨大的優勢,因為我們無須花費精力去了解對象在類型系統中的繼承關系。相反,我們只需要專注於它們的個性特征。

如果我們想要eventuality訪問該對象的私有狀態,可以把私有成員集my傳遞給它。

 

 

 

參考:

https://www.zybuluo.com/zhangzhen/note/77227

 

本文作者starof,因知識本身在變化,作者也在不斷學習成長,文章內容也不定時更新,為避免誤導讀者,方便追根溯源,請諸位轉載注明出處:http://www.cnblogs.com/starof/p/4904929.html有問題歡迎與我討論,共同進步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM