一乍惊、封裝
Javascript是一種基于對(duì)象(object-based)的語言撑柔,你遇到的所有東西幾乎都是對(duì)象艾猜。但是失暴,它又不是一種真正的面向?qū)ο缶幊蹋∣OP)語言蝴蜓,因?yàn)樗恼Z法中沒有class(類)亩进。
那么器罐,如果我們要把"屬性"(property)和"方法"(method)跟束,封裝成一個(gè)對(duì)象抬吟,甚至要從原型對(duì)象生成一個(gè)實(shí)例對(duì)象萨咕,我們應(yīng)該怎么做呢?
一火本、 生成實(shí)例對(duì)象的原始模式
假定我們把貓看成一個(gè)對(duì)象危队,它有"名字"和"顏色"兩個(gè)屬性聪建。
var Cat = {
name : '',
color : ''
}
現(xiàn)在,我們需要根據(jù)這個(gè)原型對(duì)象的規(guī)格(schema)茫陆,生成兩個(gè)實(shí)例對(duì)象金麸。
var cat1 = {}; // 創(chuàng)建一個(gè)空對(duì)象
cat1.name = "大毛"; // 按照原型對(duì)象的屬性賦值
cat1.color = "黃色";
var cat2 = {};
cat2.name = "二毛";
cat2.color = "黑色";
好了,這就是最簡(jiǎn)單的封裝了簿盅,把兩個(gè)屬性封裝在一個(gè)對(duì)象里面挥下。但是,這樣的寫法有兩個(gè)缺點(diǎn)桨醋,一是如果多生成幾個(gè)實(shí)例棚瘟,寫起來就非常麻煩;二是實(shí)例與原型之間喜最,沒有任何辦法偎蘸,可以看出有什么聯(lián)系。
二瞬内、 原始模式的改進(jìn)
我們可以寫一個(gè)函數(shù)禀苦,解決代碼重復(fù)的問題。
function Cat(name,color) {
return {
name:name,
color:color
}
}
然后生成實(shí)例對(duì)象遂鹊,就等于是在調(diào)用函數(shù):
var cat1 = Cat("大毛","黃色");
var cat2 = Cat("二毛","黑色");
這種方法的問題依然是振乏,cat1和cat2之間沒有內(nèi)在的聯(lián)系,不能反映出它們是同一個(gè)原型對(duì)象的實(shí)例秉扑。
三慧邮、 構(gòu)造函數(shù)模式
為了解決從原型對(duì)象生成實(shí)例的問題,Javascript提供了一個(gè)構(gòu)造函數(shù)(Constructor)模式舟陆。
所謂"構(gòu)造函數(shù)"误澳,其實(shí)就是一個(gè)普通函數(shù),但是內(nèi)部使用了this
變量秦躯。對(duì)構(gòu)造函數(shù)使用new
運(yùn)算符忆谓,就能生成實(shí)例,并且this
變量會(huì)綁定在實(shí)例對(duì)象上踱承。
比如倡缠,貓的原型對(duì)象現(xiàn)在可以這樣寫,
function Cat(name,color){
this.name=name;
this.color=color;
}
我們現(xiàn)在就可以生成實(shí)例對(duì)象了茎活。
var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.name); // 大毛
alert(cat1.color); // 黃色
這時(shí)cat1和cat2會(huì)自動(dòng)含有一個(gè)constructor屬性昙沦,指向它們的構(gòu)造函數(shù)。
alert(cat1.constructor == Cat); //true
alert(cat2.constructor == Cat); //true
Javascript還提供了一個(gè)instanceof運(yùn)算符载荔,驗(yàn)證原型對(duì)象與實(shí)例對(duì)象之間的關(guān)系盾饮。
alert(cat1 instanceof Cat); //true
alert(cat2 instanceof Cat); //true
四、構(gòu)造函數(shù)模式的問題
構(gòu)造函數(shù)方法很好用,但是存在一個(gè)浪費(fèi)內(nèi)存的問題丘损。
請(qǐng)看普办,我們現(xiàn)在為Cat對(duì)象添加一個(gè)不變的屬性type(種類),再添加一個(gè)方法eat(吃)徘钥。那么衔蹲,原型對(duì)象Cat就變成了下面這樣:
function Cat(name,color){
this.name = name;
this.color = color;
this.type = "貓科動(dòng)物";
this.eat = function(){alert("吃老鼠");};
}
還是采用同樣的方法,生成實(shí)例:
var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat ("二毛","黑色");
alert(cat1.type); // 貓科動(dòng)物
cat1.eat(); // 吃老鼠
表面上好像沒什么問題吏饿,但是實(shí)際上這樣做,有一個(gè)很大的弊端蔬浙。那就是對(duì)于每一個(gè)實(shí)例對(duì)象猪落,type屬性和eat()方法都是一模一樣的內(nèi)容,每一次生成一個(gè)實(shí)例畴博,都必須為重復(fù)的內(nèi)容笨忌,多占用一些內(nèi)存。這樣既不環(huán)保俱病,也缺乏效率官疲。
alert(cat1.eat == cat2.eat); //false
能不能讓type屬性和eat()方法在內(nèi)存中只生成一次,然后所有實(shí)例都指向那個(gè)內(nèi)存地址呢亮隙?回答是可以的途凫。
五、 Prototype模式
Javascript規(guī)定溢吻,每一個(gè)構(gòu)造函數(shù)都有一個(gè)prototype屬性维费,指向另一個(gè)對(duì)象。這個(gè)對(duì)象的所有屬性和方法促王,都會(huì)被構(gòu)造函數(shù)的實(shí)例繼承犀盟。
這意味著,我們可以把那些不變的屬性和方法蝇狼,直接定義在prototype對(duì)象上阅畴。
function Cat(name,color){
this.name = name;
this.color = color;
}
Cat.prototype.type = "貓科動(dòng)物";
Cat.prototype.eat = function(){alert("吃老鼠")};
然后,生成實(shí)例迅耘。
var cat1 = new Cat("大毛","黃色");
var cat2 = new Cat("二毛","黑色");
alert(cat1.type); // 貓科動(dòng)物
cat1.eat(); // 吃老鼠
這時(shí)所有實(shí)例的type屬性和eat()方法贱枣,其實(shí)都是同一個(gè)內(nèi)存地址,指向prototype對(duì)象颤专,因此就提高了運(yùn)行效率冯事。
alert(cat1.eat == cat2.eat); //true
六、 Prototype模式的驗(yàn)證方法
為了配合prototype屬性血公,Javascript定義了一些輔助方法昵仅,幫助我們使用它。,
6.1 isPrototypeOf()
這個(gè)方法用來判斷摔笤,某個(gè)proptotype對(duì)象和某個(gè)實(shí)例之間的關(guān)系够滑。
alert(Cat.prototype.isPrototypeOf(cat1)); //true
alert(Cat.prototype.isPrototypeOf(cat2)); //true
6.2 hasOwnProperty()
每個(gè)實(shí)例對(duì)象都有一個(gè)hasOwnProperty()方法,用來判斷某一個(gè)屬性到底是本地屬性吕世,還是繼承自prototype對(duì)象的屬性彰触。
alert(cat1.hasOwnProperty("name")); // true
alert(cat1.hasOwnProperty("type")); // false
6.3 in運(yùn)算符
in運(yùn)算符可以用來判斷,某個(gè)實(shí)例是否含有某個(gè)屬性命辖,不管是不是本地屬性况毅。
alert("name" in cat1); // true
alert("type" in cat1); // true
in運(yùn)算符還可以用來遍歷某個(gè)對(duì)象的所有屬性。
for(var prop in cat1) { alert("cat1["+prop+"]="+cat1[prop]); }
二尔艇、構(gòu)造函數(shù)的繼承
要介紹的是尔许,對(duì)象之間的"繼承"的五種方法。
比如终娃,現(xiàn)在有一個(gè)"動(dòng)物"對(duì)象的構(gòu)造函數(shù)味廊。
function Animal(){
this.species = "動(dòng)物";
}
還有一個(gè)"貓"對(duì)象的構(gòu)造函數(shù)。
function Cat(name,color){
this.name = name;
this.color = color;
}
怎樣才能使"貓"繼承"動(dòng)物"呢棠耕?
一余佛、 構(gòu)造函數(shù)綁定
第一種方法也是最簡(jiǎn)單的方法,使用call或apply方法窍荧,將父對(duì)象的構(gòu)造函數(shù)綁定在子對(duì)象上辉巡,即在子對(duì)象構(gòu)造函數(shù)中加一行:
function Cat(name,color){
Animal.apply(this, arguments);
this.name = name;
this.color = color;
}
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動(dòng)物
二、 prototype模式
第二種方法更常見蕊退,使用prototype屬性红氯。
如果"貓"的prototype對(duì)象,指向一個(gè)Animal的實(shí)例咕痛,那么所有"貓"的實(shí)例痢甘,就能繼承Animal了。
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動(dòng)物
代碼的第一行茉贡,我們將Cat的prototype對(duì)象指向一個(gè)Animal的實(shí)例塞栅。
Cat.prototype = new Animal();
它相當(dāng)于完全刪除了prototype 對(duì)象原先的值,然后賦予一個(gè)新值腔丧。但是放椰,第二行又是什么意思呢?
Cat.prototype.constructor = Cat;
原來愉粤,任何一個(gè)prototype對(duì)象都有一個(gè)constructor屬性砾医,指向它的構(gòu)造函數(shù)。如果沒有"Cat.prototype = new Animal();"這一行衣厘,Cat.prototype.constructor是指向Cat的如蚜;加了這一行以后压恒,Cat.prototype.constructor指向Animal。
alert(Cat.prototype.constructor == Animal); //true
更重要的是错邦,每一個(gè)實(shí)例也有一個(gè)constructor屬性探赫,默認(rèn)調(diào)用prototype對(duì)象的constructor屬性。
alert(cat1.constructor == Cat.prototype.constructor); // true
因此撬呢,在運(yùn)行"Cat.prototype = new Animal();"這一行之后伦吠,cat1.constructor也指向Animal!
alert(cat1.constructor == Animal); // true
這顯然會(huì)導(dǎo)致繼承鏈的紊亂(cat1明明是用構(gòu)造函數(shù)Cat生成的)魂拦,因此我們必須手動(dòng)糾正毛仪,將Cat.prototype對(duì)象的constructor值改為Cat。這就是第二行的意思芯勘。
這是很重要的一點(diǎn)箱靴,編程時(shí)務(wù)必要遵守。下文都遵循這一點(diǎn)借尿,即如果替換了prototype對(duì)象刨晴,
```javascript
o.prototype = {};
那么屉来,下一步必然是為新的prototype對(duì)象加上constructor屬性路翻,并將這個(gè)屬性指回原來的構(gòu)造函數(shù)。
o.prototype.constructor = o;
三茄靠、 直接繼承prototype
第三種方法是對(duì)第二種方法的改進(jìn)茂契。由于Animal對(duì)象中,不變的屬性都可以直接寫入Animal.prototype慨绳。所以掉冶,我們也可以讓Cat()跳過 Animal(),直接繼承Animal.prototype脐雪。
現(xiàn)在厌小,我們先將Animal對(duì)象改寫:
function Animal(){ }
Animal.prototype.species = "動(dòng)物";
然后,將Cat的prototype對(duì)象战秋,然后指向Animal的prototype對(duì)象璧亚,這樣就完成了繼承。
Cat.prototype = Animal.prototype;
Cat.prototype.constructor = Cat;
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動(dòng)物
與前一種方法相比脂信,這樣做的優(yōu)點(diǎn)是效率比較高(不用執(zhí)行和建立Animal的實(shí)例了)癣蟋,比較省內(nèi)存。缺點(diǎn)是 Cat.prototype和Animal.prototype現(xiàn)在指向了同一個(gè)對(duì)象狰闪,那么任何對(duì)Cat.prototype的修改疯搅,都會(huì)反映到Animal.prototype。
所以埋泵,上面這一段代碼其實(shí)是有問題的幔欧。請(qǐng)看第二行
Cat.prototype.constructor = Cat;
這一句實(shí)際上把Animal.prototype對(duì)象的constructor屬性也改掉了!
alert(Animal.prototype.constructor); // Cat
四、 利用空對(duì)象作為中介
由于"直接繼承prototype"存在上述的缺點(diǎn)琐馆,所以就有第四種方法规阀,利用一個(gè)空對(duì)象作為中介。
var F = function(){};
F.prototype = Animal.prototype;
Cat.prototype = new F();
Cat.prototype.constructor = Cat;
F是空對(duì)象瘦麸,所以幾乎不占內(nèi)存谁撼。這時(shí),修改Cat的prototype對(duì)象滋饲,就不會(huì)影響到Animal的prototype對(duì)象厉碟。
alert(Animal.prototype.constructor); // Animal
我們將上面的方法,封裝成一個(gè)函數(shù)屠缭,便于使用箍鼓。
function extend(Child, Parent) {
var F = function(){};
F.prototype = Parent.prototype;
Child.prototype = new F();
Child.prototype.constructor = Child;
Child.uber = Parent.prototype;
}
使用的時(shí)候,方法如下
extend(Cat,Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動(dòng)物
這個(gè)extend函數(shù)呵曹,就是YUI庫如何實(shí)現(xiàn)繼承的方法款咖。
另外,說明一點(diǎn)奄喂,函數(shù)體最后一行
Child.uber = Parent.prototype;
意思是為子對(duì)象設(shè)一個(gè)uber屬性铐殃,這個(gè)屬性直接指向父對(duì)象的prototype屬性。(uber是一個(gè)德語詞跨新,意思是"向上"富腊、"上一層"。)這等于在子對(duì)象上打開一條通道域帐,可以直接調(diào)用父對(duì)象的方法赘被。這一行放在這里,只是為了實(shí)現(xiàn)繼承的完備性肖揣,純屬備用性質(zhì)民假。
五、 拷貝繼承
上面是采用prototype對(duì)象龙优,實(shí)現(xiàn)繼承羊异。我們也可以換一種思路,純粹采用"拷貝"方法實(shí)現(xiàn)繼承陋率。簡(jiǎn)單說球化,如果把父對(duì)象的所有屬性和方法,拷貝進(jìn)子對(duì)象瓦糟,不也能夠?qū)崿F(xiàn)繼承嗎筒愚?這樣我們就有了第五種方法。
首先菩浙,還是把Animal的所有不變屬性巢掺,都放到它的prototype對(duì)象上句伶。
function Animal(){}
Animal.prototype.species = "動(dòng)物";
然后,再寫一個(gè)函數(shù)陆淀,實(shí)現(xiàn)屬性拷貝的目的考余。
function extend2(Child, Parent) {
var p = Parent.prototype;
var c = Child.prototype;
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
}
這個(gè)函數(shù)的作用,就是將父對(duì)象的prototype對(duì)象中的屬性轧苫,一一拷貝給Child對(duì)象的prototype對(duì)象楚堤。
使用的時(shí)候,這樣寫:
extend2(Cat, Animal);
var cat1 = new Cat("大毛","黃色");
alert(cat1.species); // 動(dòng)物
三含懊、非構(gòu)造函數(shù)的繼承
最后一個(gè)部分身冬,介紹不使用構(gòu)造函數(shù)實(shí)現(xiàn)"繼承"。
一岔乔、什么是"非構(gòu)造函數(shù)"的繼承酥筝?
比如,現(xiàn)在有一個(gè)對(duì)象雏门,叫做"中國(guó)人"嘿歌。
var Chinese = {
nation:'中國(guó)'
};
還有一個(gè)對(duì)象,叫做"醫(yī)生"茁影。
var Doctor ={
career:'醫(yī)生'
}
請(qǐng)問怎樣才能讓"醫(yī)生"去繼承"中國(guó)人"宙帝,也就是說,我怎樣才能生成一個(gè)"中國(guó)醫(yī)生"的對(duì)象呼胚?
這里要注意茄唐,這兩個(gè)對(duì)象都是普通對(duì)象息裸,不是構(gòu)造函數(shù)蝇更,無法使用構(gòu)造函數(shù)方法實(shí)現(xiàn)"繼承"。
二呼盆、object()方法
json格式的發(fā)明人Douglas Crockford年扩,提出了一個(gè)object()函數(shù),可以做到這一點(diǎn)访圃。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
這個(gè)object()函數(shù)厨幻,其實(shí)只做一件事,就是把子對(duì)象的prototype屬性腿时,指向父對(duì)象况脆,從而使得子對(duì)象與父對(duì)象連在一起。
使用的時(shí)候批糟,第一步先在父對(duì)象的基礎(chǔ)上格了,生成子對(duì)象:
var Doctor = object(Chinese);
然后,再加上子對(duì)象本身的屬性:
Doctor.career = '醫(yī)生';
這時(shí)徽鼎,子對(duì)象已經(jīng)繼承了父對(duì)象的屬性了盛末。
alert(Doctor.nation); //中國(guó)
三弹惦、淺拷貝
除了使用"prototype鏈"以外,還有另一種思路:把父對(duì)象的屬性悄但,全部拷貝給子對(duì)象棠隐,也能實(shí)現(xiàn)繼承。
下面這個(gè)函數(shù)檐嚣,就是在做拷貝:
function extendCopy(p) {
var c = {};
for (var i in p) {
c[i] = p[i];
}
c.uber = p;
return c;
}
使用的時(shí)候助泽,這樣寫:
var Doctor = extendCopy(Chinese);
Doctor.career = '醫(yī)生';
alert(Doctor.nation); // 中國(guó)
但是,這樣的拷貝有一個(gè)問題嚎京。那就是报咳,如果父對(duì)象的屬性等于數(shù)組或另一個(gè)對(duì)象,那么實(shí)際上挖藏,子對(duì)象獲得的只是一個(gè)內(nèi)存地址暑刃,而不是真正拷貝,因此存在父對(duì)象被篡改的可能膜眠。
請(qǐng)看岩臣,現(xiàn)在給Chinese添加一個(gè)"出生地"屬性,它的值是一個(gè)數(shù)組宵膨。
Chinese.birthPlaces = ['北京','上海','香港'];
通過extendCopy()函數(shù)架谎,Doctor繼承了Chinese。
var Doctor = extendCopy(Chinese);
然后辟躏,我們?yōu)镈octor的"出生地"添加一個(gè)城市:
Doctor.birthPlaces.push('廈門');
發(fā)生了什么事谷扣?Chinese的"出生地"也被改掉了!
alert(Doctor.birthPlaces); //北京, 上海, 香港, 廈門
alert(Chinese.birthPlaces); //北京, 上海, 香港, 廈門
所以捎琐,extendCopy()只是拷貝基本類型的數(shù)據(jù)会涎,我們把這種拷貝叫做"淺拷貝"。這是早期jQuery實(shí)現(xiàn)繼承的方式瑞凑。
四末秃、深拷貝
所謂"深拷貝",就是能夠?qū)崿F(xiàn)真正意義上的數(shù)組和對(duì)象的拷貝籽御。它的實(shí)現(xiàn)并不難练慕,只要遞歸調(diào)用"淺拷貝"就行了。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
使用的時(shí)候這樣寫:
var Doctor = deepCopy(Chinese);
現(xiàn)在技掏,給父對(duì)象加一個(gè)屬性铃将,值為數(shù)組。然后哑梳,在子對(duì)象上修改這個(gè)屬性:
Chinese.birthPlaces = ['北京','上海','香港'];
Doctor.birthPlaces.push('廈門');
這時(shí)劲阎,父對(duì)象就不會(huì)受到影響了。
alert(Doctor.birthPlaces); //北京, 上海, 香港, 廈門
alert(Chinese.birthPlaces); //北京, 上海, 香港
目前涧衙,jQuery庫使用的就是這種繼承方法哪工。
本文轉(zhuǎn)載自http://www.ruanyifeng.com/blog/2010/05/object-oriented_javascript_encapsulation.html