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個步驟:
- 創建一個新對象。有很多的方式去構造一個對象
- 對象字面量構造
- new調用一個構造器函數
- Object.create方法構造一個已經盡的對象的新實例
- 調用任意一個會返回一個對象的函數
- 有選擇性地定義私有實例變量和方法。這些就是函數中通過var 語句定義的普通變量。
- 給這個新對象擴充方法。這些方法擁有特權去訪問參數,以及在第二步中通過var語句定義的變量。
- 返回那個新對象。
下面是一個函數化構造器的偽代碼模板(加粗的文本表示強調):
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>
五、部件(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有問題歡迎與我討論,共同進步。