一、js的原型模式
1. 什么是原型模式?
在js里面,每個(gè)函數(shù)都有一個(gè)prototype(原型)屬性,這個(gè)屬性是一個(gè)指針,指向一個(gè)對(duì)象,而這個(gè)對(duì)象的用途是包含可以特定類型的所有實(shí)例共享的屬性和方法。如果按照字面意思來(lái)理解,那么prototype 就是通過(guò)調(diào)用構(gòu)造函數(shù)而創(chuàng)建的那個(gè)對(duì)象實(shí)例的原型對(duì)象。使用原型對(duì)象的好處是可以讓所有對(duì)象實(shí)例共享它所包含的屬性和方法。換句話說(shuō),不必在構(gòu)造函數(shù)中定義對(duì)象實(shí)例的信息,而是可以將這些信息直接添加到原型對(duì)象中,如下面的例子所示。
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"Nicholas"
var person2 = new Person();
person2.sayName(); //"Nicholas"
alert(person1.sayName == person2.sayName); //true
在這個(gè)例子中,我們將sayName()方法和所有屬性直接添加到了Person 的prototype 屬性中,構(gòu)造函數(shù)變成了空函數(shù)。即使如此,也仍然可以通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)創(chuàng)建新對(duì)象,而且新對(duì)象還會(huì)具有相同的屬性和方法。但與構(gòu)造函數(shù)模式不同的是,新對(duì)象的這些屬性和方法是由所有實(shí)例共享的。換句話說(shuō),person1 和person2 訪問(wèn)的都是同一組屬性和同一個(gè)sayName()函數(shù)。要理解原型模式的工作原理,必須先理解ECMAScript 中原型對(duì)象的性質(zhì)。
2. 什么是原型對(duì)象?
無(wú)論什么時(shí)候,只要?jiǎng)?chuàng)建了一個(gè)新函數(shù),就會(huì)根據(jù)一組特定的規(guī)則為該函數(shù)創(chuàng)建一個(gè)prototype屬性,這個(gè)屬性指向函數(shù)的原型對(duì)象。在默認(rèn)情況下,所有原型對(duì)象都會(huì)自動(dòng)獲得一個(gè)constructor(構(gòu)造函數(shù))屬性,這個(gè)屬性包含一個(gè)指向prototype 屬性所在函數(shù)的指針。就拿前面的例子來(lái)說(shuō),Person.prototype. constructor 指向Person。而通過(guò)這個(gè)構(gòu)造函數(shù),我們還可繼續(xù)為原型對(duì)象添加其他屬性和方法。
創(chuàng)建了自定義的構(gòu)造函數(shù)之后,其原型對(duì)象默認(rèn)只會(huì)取得constructor 屬性;至于其他方法,則都是從Object 繼承而來(lái)的。當(dāng)調(diào)用構(gòu)造函數(shù)創(chuàng)建一個(gè)新實(shí)例后,該實(shí)例的內(nèi)部將包含一個(gè)指針(內(nèi)部屬性),指向構(gòu)造函數(shù)的原型對(duì)象。ECMA-262 第5 版中管這個(gè)指針叫[[Prototype]]。雖然在腳本中沒(méi)有標(biāo)準(zhǔn)的方式訪問(wèn)[[Prototype]],但Firefox、Safari 和Chrome 在每個(gè)對(duì)象上都支持一個(gè)屬性proto;而在其他實(shí)現(xiàn)中,這個(gè)屬性對(duì)腳本則是完全不可見(jiàn)的。不過(guò),要明確的真正重要的一點(diǎn)就是,這個(gè)連接存在于實(shí)例與構(gòu)造函數(shù)的原型對(duì)象之間,而不是存在于實(shí)例與構(gòu)造函數(shù)之間。以前面使用Person 構(gòu)造函數(shù)和Person.prototype 創(chuàng)建實(shí)例的代碼為例,圖6-1 展示了各個(gè)對(duì)象之間的關(guān)系。

圖6-1 展示了Person 構(gòu)造函數(shù)、Person 的原型屬性以及Person 現(xiàn)有的兩個(gè)實(shí)例之間的關(guān)系。在此,Person.prototype 指向了原型對(duì)Person.prototype.constructor 又指回了Person。原型對(duì)象中除了包含constructor 屬性之外,還包括后來(lái)添加的其他屬性。Person 的每個(gè)實(shí)例——person1 和person2 都包含一個(gè)內(nèi)部屬性,該屬性僅僅指向了Person.prototype;換句話說(shuō),它們與構(gòu)造函數(shù)沒(méi)有直接的關(guān)系。此外,要格外注意的是,雖然這兩個(gè)實(shí)例都不包含屬性和方法,但我們卻可以調(diào)用person1.sayName()。這是通過(guò)查找對(duì)象屬性的過(guò)程來(lái)實(shí)現(xiàn)的。
3. 原生對(duì)象中的原型對(duì)象
原型模式的重要性不僅體現(xiàn)在創(chuàng)建自定義類型方面,就連所有原生的引用類型,都是采用這種模式創(chuàng)建的。所有原生引用類型(Object、Array、String,等等)都在其構(gòu)造函數(shù)的原型上定義了方法。例如,在Array.prototype 中可以找到sort()方法,而在String.prototype 中可以找到substring()方法,如下所示。
alert(typeof Array.prototype.sort); //"function"
alert(typeof String.prototype.substring); //"function"
通過(guò)原生對(duì)象的原型,不僅可以取得所有默認(rèn)方法的引用,而且也可以定義新方法。可以像修改自定義對(duì)象的原型一樣修改原生對(duì)象的原型,因此可以隨時(shí)添加方法。下面的代碼就給基本包裝類型String 添加了一個(gè)名為startsWith()的方法。
String.prototype.startsWith = function (text) {
return this.indexOf(text) == 0;
};
var msg = "Hello world!";
alert(msg.startsWith("Hello")); //true
這里新定義的startsWith()方法會(huì)在傳入的文本位于一個(gè)字符串開(kāi)始時(shí)返回true。既然方法被添加給了String.prototype,那么當(dāng)前環(huán)境中的所有字符串就都可以調(diào)用它。由于msg 是字符串,而且后臺(tái)會(huì)調(diào)用String 基本包裝函數(shù)創(chuàng)建這個(gè)字符串,因此通過(guò)msg 就可以調(diào)用startsWith()方法。
二、js的原型鏈和繼承
繼承是OO 語(yǔ)言中的一個(gè)最為人津津樂(lè)道的概念。許多OO 語(yǔ)言都支持兩種繼承方式:接口繼承和實(shí)現(xiàn)繼承。接口繼承只繼承方法簽名,而實(shí)現(xiàn)繼承則繼承實(shí)際的方法。如前所述,由于函數(shù)沒(méi)有簽名,在ECMAScript 中無(wú)法實(shí)現(xiàn)接口繼承。ECMAScript 只支持實(shí)現(xiàn)繼承,而且其實(shí)現(xiàn)繼承主要是依靠原型鏈來(lái)實(shí)現(xiàn)的。
1、原型鏈
ECMAScript 中描述了原型鏈的概念,并將原型鏈作為實(shí)現(xiàn)繼承的主要方法。其基本思想是利用原型讓一個(gè)引用類型繼承另一個(gè)引用類型的屬性和方法。簡(jiǎn)單回顧一下構(gòu)造函數(shù)、原型和實(shí)例的關(guān)系:每個(gè)構(gòu)造函數(shù)都有一個(gè)原型對(duì)象,原型對(duì)象都包含一個(gè)指向構(gòu)造函數(shù)的針,而實(shí)例都包含一個(gè)指向原型對(duì)象的內(nèi)部指針。那么,假如我們讓原型對(duì)象等于另一個(gè)類型的實(shí)例,結(jié)果會(huì)怎么樣呢?顯然,此時(shí)的原型對(duì)象將包含一個(gè)指向另一個(gè)原型的指針,相應(yīng)地,另一個(gè)原型中也包含著一個(gè)指向另一個(gè)構(gòu)造函數(shù)的指針。假如另一個(gè)原型又是另一個(gè)類型的實(shí)例,那么上述關(guān)系依然成立,如此層層遞進(jìn),就構(gòu)成了實(shí)例與原型的鏈條。這就是所謂原型鏈的基本概念。
實(shí)現(xiàn)原型鏈有一種基本模式,其代碼大致如下。
//父類
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
//子類
function SubType(){
this.subproperty = false;
}
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
//怎么實(shí)現(xiàn)繼承
SubType.prototype = new SuperType();
var instance = new SubType();
alert(instance.getSuperValue()); //true
以上代碼定義了兩個(gè)類型:SuperType 和SubType。每個(gè)類型分別有一個(gè)屬性和一個(gè)方法。它們的主要區(qū)別是SubType 繼承SuperType,而繼承是通過(guò)創(chuàng)建SuperType 的實(shí)例,并將該實(shí)例賦給了SubType.prototype 實(shí)現(xiàn)的。實(shí)現(xiàn)的本質(zhì)是重寫(xiě)原型對(duì)象,代之以一個(gè)新類型的實(shí)例。換句話說(shuō),原來(lái)存在于SuperType 的實(shí)例中的所有屬性和方法,現(xiàn)在也存在于SubType.prototype 中了。在確立了繼承關(guān)系之后,我們給SubType.prototype 添加了一個(gè)方法,這樣就在繼承了SuperType 的屬性和方法的基礎(chǔ)上又添加了一個(gè)新方法。這個(gè)例子中的實(shí)例以及構(gòu)造函數(shù)和原型之間的關(guān)系如圖6-4 所示。

在上面的代碼中,我們沒(méi)有使用SubType 默認(rèn)提供的原型,而是給它換了一個(gè)新原型;這個(gè)新原型就是SuperType 的實(shí)例。于是,新原型不僅具有作為一個(gè)SuperType 的實(shí)例所擁有的全部屬性和方法,而且其內(nèi)部還有一個(gè)指針,指向了SuperType 的原型。最終結(jié)果就是這樣的:instance 指向SubType的原型, SubType 的原型又指向SuperType 的原型。getSuperValue() 方法仍然還在SuperType.prototype 中,但property 則位于SubType.prototype 中。這是因?yàn)閜roperty 是一個(gè)實(shí)例屬性,而getSuperValue()則是一個(gè)原型方法。既然SubType.prototype 現(xiàn)在是SuperType的實(shí)例,那么property 當(dāng)然就位于該實(shí)例中了。此外,要注instance.constructor 現(xiàn)在指向的是SuperType,這是因?yàn)樵瓉?lái)SubType.prototype 中的constructor 被重寫(xiě)了的緣故①。
通過(guò)實(shí)現(xiàn)原型鏈,本質(zhì)上擴(kuò)展了本章前面介紹的原型搜索機(jī)制。讀者大概還記得,當(dāng)以讀取模式訪問(wèn)一個(gè)實(shí)例屬性時(shí),首先會(huì)在實(shí)例中搜索該屬性。如果沒(méi)有找到該屬性,則會(huì)繼續(xù)搜索實(shí)例的原型。在通過(guò)原型鏈實(shí)現(xiàn)繼承的情況下,搜索過(guò)程就得以沿著原型鏈繼續(xù)向上。就拿上面的例子來(lái)說(shuō),調(diào)用instance.getSuperValue()會(huì)經(jīng)歷三個(gè)搜索步驟:1)搜索實(shí)例;2)搜索SubType.prototype;3)搜索SuperType.prototype,最后一步才會(huì)找到該方法。在找不到屬性或方法的情況下,搜索過(guò)程總是要一環(huán)一環(huán)地前行到原型鏈末端才會(huì)停下來(lái)。