當(dāng)我剛開始接觸Javascript的時(shí)候,因?yàn)樗瘮?shù)式腳本語言特性以及看似隨意的編寫風(fēng)格毁枯,讓像我這種只用過Java和C#程序員耳(yi)目(lian)一(meng)新(bi)蚯根,那時(shí)候用的最多的就是alert颅拦,囧。這些年Web大行其道右锨,各種應(yīng)用對(duì)Javascript的依賴日深绍移。有大量優(yōu)秀的javascript框架可以讓我們快速開發(fā)web應(yīng)用讥电,實(shí)際上對(duì)原生javascript卻知之甚少恩敌。所以經(jīng)常出現(xiàn)做了多年JS開發(fā)的程序員,對(duì)this月趟,閉包孝宗,原型這些javascript基礎(chǔ)知識(shí)總是說不清楚的情況。
寫這篇文章的目的问潭,是為了幫助像我這樣使用過JS框架但是對(duì)Javascript面向?qū)ο笠恢虢獾某绦騿T能夠?qū)avascript的面向?qū)ο笥袀€(gè)清晰的認(rèn)識(shí)睦授。
什么是面向?qū)ο?/h1>
大家都知道JavaScript是面向?qū)ο蟮某绦蛘Z言摔寨,但是又與一般的基于類的面向?qū)ο笳Z言不同是复。
ECMA-162把對(duì)象定義為:“無序?qū)傩缘募希鋵傩钥梢园局刀河唷?duì)象或是函數(shù)录粱』埃”
理解對(duì)象
最基本的面向?qū)ο?/h3>
創(chuàng)建自定義對(duì)象最簡單的方式就是創(chuàng)建一個(gè)Object實(shí)例青抛,然后再為它添加屬性和方法。
var person = new Object();
person.name = 'Danny';
person.age = 18;
person.sayName = function() {
console.log('Hi, I am '+this.name);
}
前面的例子也可以字面量聲明(literral notation)的方式創(chuàng)建
var person = {
name = 'Danny',
age = 18,
sayName = function() {
console.log('Hi, I am '+this.name);
}
}
在實(shí)際的開發(fā)過程中适室,我們用上面兩種創(chuàng)建對(duì)象的方式就能滿足基本的開發(fā)需求了捣辆。
工廠模式
雖然Object構(gòu)造函數(shù)和字面量都可以創(chuàng)建單個(gè)對(duì)象此迅,但是這樣的代碼復(fù)用性太差,為解決這個(gè)問題,人們開始引用工廠模式坐昙。工廠模式抽象了創(chuàng)建具體對(duì)象的過程芋忿。
function createPerson(name,age) {
var o = new Object();
o.name = name;
o.age = age;
o.sayName = function() {
console.log('Hi, I am ' + this.name);
}
return o;
}
createPerson函數(shù)根據(jù)傳來的參數(shù)創(chuàng)建一個(gè)新的Person對(duì)象戈钢。這樣殉了,每當(dāng)我們需要一個(gè)Person的時(shí)候,傳入相應(yīng)的參數(shù)就可以了众弓,不用再像之前那樣每次都要重寫相似的代碼隔箍。
構(gòu)造函數(shù)模式
ECMAScript允許通過構(gòu)造函數(shù)來創(chuàng)建對(duì)象蜒滩。
function Person(name,age) {
this.name = name;
this.age = age;
this.sayName = function() {
console.log('Hi, I am ' + this.name);
}
}
var p1 = new Person('Danny',18);
這個(gè)Person函數(shù)和之前createPerson函數(shù)有相似的地方俯艰,比如都添加了兩個(gè)屬性一個(gè)方法,我們重點(diǎn)看看他們不同的地方:
- 沒有顯式地創(chuàng)建對(duì)象
- 直接降屬性和方法賦給了this對(duì)象
- 沒有return語句
要?jiǎng)?chuàng)建Person的實(shí)例稽莉,必須使用new操作符污秆。大家在這里不要和Java或是C#的new操作符弄混了昧甘,雖然看上去它們創(chuàng)建對(duì)象的方式很類似充边,但是它們的實(shí)現(xiàn)原理完全不一樣,這塊有機(jī)會(huì)會(huì)單獨(dú)寫篇文章來說贬媒。插句話际乘,Javascript最開始叫l(wèi)ivescript脖含,感覺自己不夠響亮,為了和當(dāng)時(shí)非痴骺埃火的Javas(1995年的時(shí)候)拉上關(guān)系才改的名字关拒,實(shí)際上它們的關(guān)系就好像阿迪王和阿迪達(dá)斯着绊。我們看看在Javascirpt中new操作符實(shí)際做了啥:
- 創(chuàng)建一個(gè)新對(duì)象
- 將構(gòu)造函數(shù)的作用域賦給新對(duì)象(所以this就指向了這個(gè)新對(duì)象)
- 執(zhí)行構(gòu)造函數(shù)中的代碼
- 返回新對(duì)象
將構(gòu)造函數(shù)當(dāng)做函數(shù)
這時(shí)候大家就會(huì)問這個(gè)構(gòu)造函數(shù)和其他的函數(shù)有啥區(qū)別呢畔柔?答案是沒啥區(qū)別靶擦,構(gòu)造函數(shù)沒有任何特殊的語法。任何函數(shù)踩蔚,只要用new操作符來調(diào)用馅闽,就可以當(dāng)做構(gòu)造函數(shù)馍迄;如果不用new操作符來調(diào)用攀圈,那它就是個(gè)普通的函數(shù)赘来。像前面的Person函數(shù)凯傲,如果我們?cè)谌肿饔糜蛑姓{(diào)用它冰单,this對(duì)象指向全局對(duì)象灸促,在瀏覽器中就會(huì)給window對(duì)象添加name和age屬性腿宰,以及sayName方法吃度。
構(gòu)造函數(shù)的問題
構(gòu)造函數(shù)雖然好椿每,但是也有自身的問題英遭,就是每個(gè)方法都要在實(shí)例中重新創(chuàng)建一遍挖诸。然而,創(chuàng)建兩個(gè)完成同樣功能的方法實(shí)例是沒有必要的痴突。好在這個(gè)問題可以通過原型模式解決辽装。
原型模式
我們創(chuàng)建的每個(gè)函數(shù)都有一個(gè)prototype(原型)屬性拾积,這個(gè)屬性是一個(gè)指針丰涉,指向一個(gè)對(duì)象一死,而這個(gè)對(duì)象的用途是包含可以由特定類型的所有勢(shì)力共享的屬性和方法摘符,這個(gè)對(duì)象稱為原型對(duì)象,簡稱原型瘩绒。
理解原型對(duì)象
function Person(){
};
Person.prototype.name = "Danny";
Person.prototype.sayName = function () {
console.log(this.name);
}
在默認(rèn)情況下蟀给,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)contructor(構(gòu)造函數(shù))屬性阳堕,指向prototype屬性所在的函數(shù)恬总。如上圖壹堰。
每個(gè)由構(gòu)造函數(shù)創(chuàng)建的實(shí)例都擁有一個(gè)指向構(gòu)造函數(shù)propotype屬性的指針贱纠,換句話說就是指向構(gòu)造函數(shù)原型的指針。ECMA-262把這個(gè)指針叫[[Prototype]]惠桃,在腳本中沒有標(biāo)準(zhǔn)的方式訪問[[Prototype]]刽射,但是FireFox, Safari和Chrome在每個(gè)對(duì)象上都支持一個(gè)屬性proto剃执。我們需要反復(fù)強(qiáng)調(diào)的就是肾档,這個(gè)連接存在于實(shí)例和構(gòu)造函數(shù)的原型對(duì)象之間怒见,而不是實(shí)例與構(gòu)造函數(shù)之間遣耍。
var person1 = new Person();
var person2 = new Person();
person1.sayName(); //Danny
person2.sayName(); //Danny
上圖展示了Person構(gòu)造函數(shù)舵变、Person的原型對(duì)象以及Person兩個(gè)實(shí)例之間的關(guān)系。雖然這兩個(gè)實(shí)例沒有任何屬性和方法扛或,但是可以通過搜索對(duì)象的過程來實(shí)現(xiàn)碘饼。每當(dāng)代碼讀取某個(gè)對(duì)象的某個(gè)屬性時(shí)艾恼,搜索首先從對(duì)象實(shí)例本身開始钠绍;如果找到則返回五慈;如果沒有主穗,則搜索[[Prototype]]指向的原型對(duì)象忽媒,如果找到則返回晦雨。所以闹瞧,我們?cè)谡{(diào)用person1.sayName的時(shí)候就會(huì)執(zhí)行兩次搜索,調(diào)用person2.sayName的時(shí)候万牺,會(huì)執(zhí)行同樣的搜索過程脚粟,得到同樣的結(jié)果核无。這也正是多個(gè)對(duì)象實(shí)例共享原型所保存的屬性和方法的基本原理团南。
我們可以通過對(duì)象實(shí)例訪問在原型中的值,但是不能通過對(duì)象實(shí)例重寫原型中的值曲聂。當(dāng)我們?cè)趯?duì)象實(shí)例中創(chuàng)建了一個(gè)屬性與原型中的屬性同名是朋腋,實(shí)例中的屬性會(huì)屏蔽原型中的屬性旭咽。
var person1 = new Person();
var person2 = new Person();
person1.name = 'Eric';
person1.sayName(); //Eric
person2.sayName(); //Danny
在上面的代碼中赌厅,person1添加了一個(gè)name屬性特愿,這個(gè)name屬性會(huì)屏蔽實(shí)例的name屬性揍障。所以我們看到的結(jié)果毒嫡,“Eric”來自實(shí)例兜畸,“Danny”來自原型咬摇。
更簡單的原型語法
為了簡單我們也可以用字面量的方式重寫原型對(duì)象。
function Person(){
};
Person.prototype = {
name : "Danny",
sayName : function () {
console.log(this.name);
}
}
在上面的代碼中饿自,我們實(shí)際上將Person.prototype設(shè)置為一個(gè)以對(duì)象字面量方式創(chuàng)建的新對(duì)象。最終的結(jié)果相同龄坪,但是有一個(gè)例外:constructor屬性不再指向Person了昭雌。我們前面說過,在默認(rèn)情況下健田,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)contructor(構(gòu)造函數(shù))屬性烛卧,指向prototype屬性所在的函數(shù)。那constructor現(xiàn)在指向的是什么呢?
我們看到指向的是Object的構(gòu)造函數(shù)总放。為啥是這樣呢呈宇?我們知道字面量等同于下面的方式局雄。
var person = new Object();
person.name = 'Danny';
person.age = 18;
person.sayName = function() {
console.log('Hi, I am '+this.name);
}
也就是說Person.prototype的構(gòu)造函數(shù)是Object甥啄,Person.prototype的[[Prototype]]指向了Object構(gòu)造函數(shù)的prototype,關(guān)系如下圖:
如果constructor的值很重要的話炬搭,可以通過下面的方式設(shè)置回適當(dāng)?shù)闹怠?/p>
Person.prototype = {
constructor:Person,
name : "Danny",
sayName : function () {
console.log(this.name);
}
}
原型的動(dòng)態(tài)性
由于在原型中查找值得過程試一次搜索蜈漓,因此我們對(duì)原型對(duì)象所做的任何修改都能夠立即從實(shí)例上反映出來。
function Person(){
};
var person1 = new Person();
Person.prototype.sayHi = function () {
console.log('Hi');
};
person1.sayHi(); //Hi
上面的代碼先創(chuàng)建了一個(gè)Person的實(shí)例person1宫盔,然后給Person.prototype添加了一個(gè)sayHi的方法融虽。person1雖然是在添加新方法之前創(chuàng)建的,但是仍然可以調(diào)用sayHi這個(gè)方法灼芭。原因是當(dāng)我們調(diào)用person1.sayHi方法時(shí)有额,首先在person1實(shí)例中查找,當(dāng)沒有找到時(shí)彼绷,就會(huì)繼續(xù)搜索原型巍佑。由于實(shí)例和原型之間的連接是一個(gè)指針,而不是副本寄悯,所以原型的變化隨時(shí)都可以在實(shí)例中反映出來萤衰。
我們?cè)賮砜纯聪旅娴睦樱?/p>
function Person(){
};
var person1 = new Person();
Person.prototype = {
constructor: Person,
name: "Danny",
age: 18,
sayName: function() {
console.log(this.name);
}
};
person1.sayName(); \\error: person1.sayName is not a function
上面執(zhí)行person1.sayName()時(shí)會(huì)報(bào)錯(cuò)。因?yàn)檎{(diào)用構(gòu)造函數(shù)時(shí)會(huì)為實(shí)例添加一個(gè)指向最初原型的[[Prototype]]指針热某,而把原型修改為另外一個(gè)對(duì)象就等于切斷了構(gòu)造函數(shù)與最初原型之間的聯(lián)系腻菇。
原型對(duì)象的問題
原型對(duì)象也不是沒有缺點(diǎn)胳螟。
- 它省略了為構(gòu)造函數(shù)傳遞參數(shù)這一環(huán)節(jié)昔馋。
- 原型中所有屬性都是共享的,而現(xiàn)實(shí)中原型想要有屬于自己的屬性糖耸。
這些問題似的我們很少看到有人單獨(dú)使用原型模式秘遏。
組合使用構(gòu)造函數(shù)模式和原型模式
很好理解,構(gòu)造函數(shù)模式用于定義實(shí)例屬性嘉竟,而原型模式用于定義方法和共享的屬性邦危。這種組合模式也支持想構(gòu)造函數(shù)傳遞參數(shù),可謂是集兩種模式之長舍扰。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); //"Shelby,Count,Van"
console.log(person2.friends); //"Shelby,Count"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
這種組合模式是ECMAScript中使用最廣泛倦蚪,認(rèn)同度最高的一種創(chuàng)建自定義類型的模式。
動(dòng)態(tài)原型模式
有其他OO語言經(jīng)驗(yàn)的開發(fā)人員在看到獨(dú)立的構(gòu)造函數(shù)和原型時(shí)边苹,很可能會(huì)感到非常困惑陵且。動(dòng)態(tài)原型模式正是為了解決這個(gè)問題的一個(gè)方案,它把所有信息都封裝在了構(gòu)造函數(shù)中个束,在構(gòu)造函數(shù)中初始化原型慕购。我們用動(dòng)態(tài)原型模式改造一下之前的代碼聊疲。
function Person(name,age,job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
if(typeof this.sayName != "function"){
Person.prototype.sayName = function() {
console.log(this.name);
}
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
console.log(person1.friends); //"Shelby,Count,Van"
console.log(person2.friends); //"Shelby,Count"
console.log(person1.friends === person2.friends); //false
console.log(person1.sayName === person2.sayName); //true
使用動(dòng)態(tài)原型時(shí),不能使用對(duì)象字面量重寫原型沪悲,這個(gè)原因前面說過了获洲。