使用預(yù)定義對象只是面向?qū)ο笳Z言的能力的一部分,它真正強(qiáng)大之處在于能夠創(chuàng)建自己專用的類和對象。
ECMAScript 擁有很多創(chuàng)建對象或類的方法。
因?yàn)閷ο蟮膶傩钥梢栽趯ο髣?chuàng)建后動態(tài)定義,所有許多開發(fā)者都在 JavaScript 最初引入時編寫類似下面的代碼:
var oCar = new Object; oCar.color = "blue"; oCar.doors = 4; oCar.mpg = 25; oCar.showColor = function() { alert(this.color); };
在上面的代碼中,創(chuàng)建對象 oCar。然后給它設(shè)置幾個屬性:它的顏色是藍(lán)色,有四個門,每加侖油可以跑 25 英里。最后一個屬性實(shí)際上是指向函數(shù)的指針,意味著該屬性是個方法。執(zhí)行這段代碼后,就可以使用對象 oCar。
不過這里有一個問題,就是可能需要創(chuàng)建多個 car 的實(shí)例。
要解決該問題,開發(fā)者創(chuàng)造了能創(chuàng)建并返回特定類型的對象的工廠函數(shù)(factory function)。
例如,函數(shù) createCar() 可用于封裝前面列出的創(chuàng)建 car 對象的操作:
function createCar() { var oTempCar = new Object; oTempCar.color = "blue"; oTempCar.doors = 4; oTempCar.mpg = 25; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar(); var oCar2 = createCar();
在這里,第一個例子中的所有代碼都包含在 createCar() 函數(shù)中。此外,還有一行額外的代碼,返回 car 對象(oTempCar)作為函數(shù)值。調(diào)用此函數(shù),將創(chuàng)建新對象,并賦予它所有必要的屬性,復(fù)制出一個我們在前面說明過的 car 對象。因此,通過這種方法,我們可以很容易地創(chuàng)建 car 對象的兩個版本(oCar1 和 oCar2),它們的屬性完全一樣。
我們還可以修改 createCar() 函數(shù),給它傳遞各個屬性的默認(rèn)值,而不是簡單地賦予屬性默認(rèn)值:
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg; oTempCar.showColor = function() { alert(this.color); }; return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //輸出 "red" oCar2.showColor(); //輸出 "blue"
給 createCar() 函數(shù)加上參數(shù),即可為要創(chuàng)建的 car 對象的 color、doors 和 mpg 屬性賦值。這使兩個對象具有相同的屬性,卻有不同的屬性值。
雖然 ECMAScript 越來越正式化,但創(chuàng)建對象的方法卻被置之不理,且其規(guī)范化至今還遭人反對。一部分是語義上的原因(它看起來不像使用帶有構(gòu)造函數(shù) new 運(yùn)算符那么正規(guī)),一部分是功能上的原因。功能原因在于用這種方式必須創(chuàng)建對象的方法。前面的例子中,每次調(diào)用函數(shù) createCar(),都要創(chuàng)建新函數(shù) showColor(),意味著每個對象都有自己的 showColor() 版本。而事實(shí)上,每個對象都共享同一個函數(shù)。
有些開發(fā)者在工廠函數(shù)外定義對象的方法,然后通過屬性指向該方法,從而避免這個問題:
function showColor() { alert(this.color); }
function createCar(sColor,iDoors,iMpg) { var oTempCar = new Object; oTempCar.color = sColor; oTempCar.doors = iDoors; oTempCar.mpg = iMpg;oTempCar.showColor = showColor;
return oTempCar; } var oCar1 = createCar("red",4,23); var oCar2 = createCar("blue",3,25); oCar1.showColor(); //輸出 "red" oCar2.showColor(); //輸出 "blue"
在上面這段重寫的代碼中,在函數(shù) createCar() 之前定義了函數(shù) showColor()。在 createCar() 內(nèi)部,賦予對象一個指向已經(jīng)存在的 showColor() 函數(shù)的指針。從功能上講,這樣解決了重復(fù)創(chuàng)建函數(shù)對象的問題;但是從語義上講,該函數(shù)不太像是對象的方法。
所有這些問題都引發(fā)了開發(fā)者定義的構(gòu)造函數(shù)的出現(xiàn)。
創(chuàng)建構(gòu)造函數(shù)就像創(chuàng)建工廠函數(shù)一樣容易。第一步選擇類名,即構(gòu)造函數(shù)的名字。根據(jù)慣例,這個名字的首字母大寫,以使它與首字母通常是小寫的變量名分開。除了這點(diǎn)不同,構(gòu)造函數(shù)看起來很像工廠函數(shù)。請考慮下面的例子:
function Car(sColor,iDoors,iMpg) {this
.color = sColor;this
.doors = iDoors;this
.mpg = iMpg;this
.showColor = function() { alert(this.color); }; } var oCar1 =new
Car("red",4,23); var oCar2 =new
Car("blue",3,25);
下面為您解釋上面的代碼與工廠方式的差別。首先在構(gòu)造函數(shù)內(nèi)沒有創(chuàng)建對象,而是使用 this 關(guān)鍵字。使用 new 運(yùn)算符構(gòu)造函數(shù)時,在執(zhí)行第一行代碼前先創(chuàng)建一個對象,只有用 this 才能訪問該對象。然后可以直接賦予 this 屬性,默認(rèn)情況下是構(gòu)造函數(shù)的返回值(不必明確使用 return 運(yùn)算符)。
現(xiàn)在,用 new 運(yùn)算符和類名 Car 創(chuàng)建對象,就更像 ECMAScript 中一般對象的創(chuàng)建方式了。
你也許會問,這種方式在管理函數(shù)方面是否存在于前一種方式相同的問題呢?是的。
就像工廠函數(shù),構(gòu)造函數(shù)會重復(fù)生成函數(shù),為每個對象都創(chuàng)建獨(dú)立的函數(shù)版本。不過,與工廠函數(shù)相似,也可以用外部函數(shù)重寫構(gòu)造函數(shù),同樣地,這么做語義上無任何意義。這正是下面要講的原型方式的優(yōu)勢所在。
該方式利用了對象的 prototype 屬性,可以把它看成創(chuàng)建新對象所依賴的原型。
這里,首先用空構(gòu)造函數(shù)來設(shè)置類名。然后所有的屬性和方法都被直接賦予 prototype 屬性。我們重寫了前面的例子,代碼如下:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25; Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();
在這段代碼中,首先定義構(gòu)造函數(shù)(Car),其中無任何代碼。接下來的幾行代碼,通過給 Car 的 prototype 屬性添加屬性去定義 Car 對象的屬性。調(diào)用 new Car() 時,原型的所有屬性都被立即賦予要創(chuàng)建的對象,意味著所有 Car 實(shí)例存放的都是指向 showColor() 函數(shù)的指針。從語義上講,所有屬性看起來都屬于一個對象,因此解決了前面兩種方式存在的問題。
此外,使用這種方式,還能用 instanceof 運(yùn)算符檢查給定變量指向的對象的類型。因此,下面的代碼將輸出 TRUE:
alert(oCar1 instanceof Car); //輸出 "true"
原型方式看起來是個不錯的解決方案。遺憾的是,它并不盡如人意。
首先,這個構(gòu)造函數(shù)沒有參數(shù)。使用原型方式,不能通過給構(gòu)造函數(shù)傳遞參數(shù)來初始化屬性的值,因?yàn)?Car1 和 Car2 的 color 屬性都等于 "blue",doors 屬性都等于 4,mpg 屬性都等于 25。這意味著必須在對象創(chuàng)建后才能改變屬性的默認(rèn)值,這點(diǎn)很令人討厭,但還沒完。真正的問題出現(xiàn)在屬性指向的是對象,而不是函數(shù)時。函數(shù)共享不會造成問題,但對象卻很少被多個實(shí)例共享。請思考下面的例子:
function Car() { } Car.prototype.color = "blue"; Car.prototype.doors = 4; Car.prototype.mpg = 25;Car.prototype.drivers = new Array("Mike","John");
Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car(); var oCar2 = new Car();oCar1.drivers.push("Bill");
alert(oCar1.drivers); //輸出 "Mike,John,Bill" alert(oCar2.drivers); //輸出 "Mike,John,Bill"
上面的代碼中,屬性 drivers 是指向 Array 對象的指針,該數(shù)組中包含兩個名字 "Mike" 和 "John"。由于 drivers 是引用值,Car 的兩個實(shí)例都指向同一個數(shù)組。這意味著給 oCar1.drivers 添加值 "Bill",在 oCar2.drivers 中也能看到。輸出這兩個指針中的任何一個,結(jié)果都是顯示字符串 "Mike,John,Bill"。
由于創(chuàng)建對象時有這么多問題,你一定會想,是否有種合理的創(chuàng)建對象的方法呢?答案是有,需要聯(lián)合使用構(gòu)造函數(shù)和原型方式。
聯(lián)合使用構(gòu)造函數(shù)和原型方式,就可像用其他程序設(shè)計語言一樣創(chuàng)建對象。這種概念非常簡單,即用構(gòu)造函數(shù)定義對象的所有非函數(shù)屬性,用原型方式定義對象的函數(shù)屬性(方法)。結(jié)果是,所有函數(shù)都只創(chuàng)建一次,而每個對象都具有自己的對象屬性實(shí)例。
我們重寫了前面的例子,代碼如下:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); } Car.prototype.showColor = function() { alert(this.color); }; var oCar1 = new Car("red",4,23); var oCar2 = new Car("blue",3,25); oCar1.drivers.push("Bill"); alert(oCar1.drivers); //輸出 "Mike,John,Bill" alert(oCar2.drivers); //輸出 "Mike,John"
現(xiàn)在就更像創(chuàng)建一般對象了。所有的非函數(shù)屬性都在構(gòu)造函數(shù)中創(chuàng)建,意味著又能夠用構(gòu)造函數(shù)的參數(shù)賦予屬性默認(rèn)值了。因?yàn)橹粍?chuàng)建 showColor() 函數(shù)的一個實(shí)例,所以沒有內(nèi)存浪費(fèi)。此外,給 oCar1 的 drivers 數(shù)組添加 "Bill" 值,不會影響到 oCar2 的數(shù)組,所以輸出這些數(shù)組的值時,oCar1.drivers 顯示的是 "Mike,John,Bill",而 oCar2.drivers 顯示的是 "Mike,John"。因?yàn)槭褂昧嗽头绞?,所以仍然能利?instanceof 運(yùn)算符來判斷對象的類型。
這種方式是 ECMAScript 采用的主要方式,它具有其他方式的特性,卻沒有他們的副作用。不過,有些開發(fā)者仍覺得這種方法不夠完美。
對于習(xí)慣使用其他語言的開發(fā)者來說,使用混合的構(gòu)造函數(shù)/原型方式感覺不那么和諧。畢竟,定義類時,大多數(shù)面向?qū)ο笳Z言都對屬性和方法進(jìn)行了視覺上的封裝。請考慮下面的 Java 類:
class Car { public String color = "blue"; public int doors = 4; public int mpg = 25; public Car(String color, int doors, int mpg) { this.color = color; this.doors = doors; this.mpg = mpg; } public void showColor() { System.out.println(color); } }
Java 很好地打包了 Car 類的所有屬性和方法,因此看見這段代碼就知道它要實(shí)現(xiàn)什么功能,它定義了一個對象的信息。批評混合的構(gòu)造函數(shù)/原型方式的人認(rèn)為,在構(gòu)造函數(shù)內(nèi)部找屬性,在其外部找方法的做法不合邏輯。因此,他們設(shè)計了動態(tài)原型方法,以提供更友好的編碼風(fēng)格。
動態(tài)原型方法的基本想法與混合的構(gòu)造函數(shù)/原型方式相同,即在構(gòu)造函數(shù)內(nèi)定義非函數(shù)屬性,而函數(shù)屬性則利用原型屬性定義。唯一的區(qū)別是賦予對象方法的位置。下面是用動態(tài)原型方法重寫的 Car 類:
function Car(sColor,iDoors,iMpg) { this.color = sColor; this.doors = iDoors; this.mpg = iMpg; this.drivers = new Array("Mike","John"); if (typeof Car._initialized == "undefined"
) { Car.prototype.showColor = function() { alert(this.color); };Car._initialized = true;
} }
直到檢查 typeof Car._initialized 是否等于 "undefined" 之前,這個構(gòu)造函數(shù)都未發(fā)生變化。這行代碼是動態(tài)原型方法中最重要的部分。如果這個值未定義,構(gòu)造函數(shù)將用原型方式繼續(xù)定義對象的方法,然后把 Car._initialized 設(shè)置為 true。如果這個值定義了(它的值為 true 時,typeof 的值為 Boolean),那么就不再創(chuàng)建該方法。簡而言之,該方法使用標(biāo)志(_initialized)來判斷是否已給原型賦予了任何方法。該方法只創(chuàng)建并賦值一次,傳統(tǒng)的 OOP 開發(fā)者會高興地發(fā)現(xiàn),這段代碼看起來更像其他語言中的類定義了。
這種方式通常是在不能應(yīng)用前一種方式時的變通方法。它的目的是創(chuàng)建假構(gòu)造函數(shù),只返回另一種對象的新實(shí)例。
這段代碼看起來與工廠函數(shù)非常相似:
function Car() {
var oTempCar = new Object;
oTempCar.color = "blue";
oTempCar.doors = 4;
oTempCar.mpg = 25;
oTempCar.showColor = function() {
alert(this.color);
};
return oTempCar;
}
與經(jīng)典方式不同,這種方式使用 new 運(yùn)算符,使它看起來像真正的構(gòu)造函數(shù):
var car = new Car();
由于在 Car() 構(gòu)造函數(shù)內(nèi)部調(diào)用了 new 運(yùn)算符,所以將忽略第二個 new 運(yùn)算符(位于構(gòu)造函數(shù)之外),在構(gòu)造函數(shù)內(nèi)部創(chuàng)建的對象被傳遞回變量 car。
這種方式在對象方法的內(nèi)部管理方面與經(jīng)典方式有著相同的問題。強(qiáng)烈建議:除非萬不得已,還是避免使用這種方式。
如前所述,目前使用最廣泛的是混合的構(gòu)造函數(shù)/原型方式。此外,動態(tài)原始方法也很流行,在功能上與構(gòu)造函數(shù)/原型方式等價??梢圆捎眠@兩種方式中的任何一種。不過不要單獨(dú)使用經(jīng)典的構(gòu)造函數(shù)或原型方式,因?yàn)檫@樣會給代碼引入問題。
對象令人感興趣的一點(diǎn)是用它們解決問題的方式。ECMAScript 中最常見的一個問題是字符串連接的性能。與其他語言類似,ECMAScript 的字符串是不可變的,即它們的值不能改變。請考慮下面的代碼:
var str = "hello "; str += "world";
實(shí)際上,這段代碼在幕后執(zhí)行的步驟如下:
每次完成字符串連接都會執(zhí)行步驟 2 到 6,使得這種操作非常消耗資源。如果重復(fù)這一過程幾百次,甚至幾千次,就會造成性能問題。解決方法是用 Array 對象存儲字符串,然后用 join() 方法(參數(shù)是空字符串)創(chuàng)建最后的字符串。想象用下面的代碼代替前面的代碼:
var arr = new Array(); arr[0] = "hello "; arr[1] = "world"; var str = arr.join("");
這樣,無論數(shù)組中引入多少字符串都不成問題,因?yàn)橹辉谡{(diào)用 join() 方法時才會發(fā)生連接操作。此時,執(zhí)行的步驟如下:
雖然這種解決方案很好,但還有更好的方法。問題是,這段代碼不能確切反映出它的意圖。要使它更容易理解,可以用 StringBuffer 類打包該功能:
function StringBuffer () { this._strings_ = new Array(); } StringBuffer.prototype.append = function(str) { this._strings_.push(str); }; StringBuffer.prototype.toString = function() { return this._strings_.join(""); };
這段代碼首先要注意的是 strings 屬性,本意是私有屬性。它只有兩個方法,即 append() 和 toString() 方法。append() 方法有一個參數(shù),它把該參數(shù)附加到字符串?dāng)?shù)組中,toString() 方法調(diào)用數(shù)組的 join 方法,返回真正連接成的字符串。要用 StringBuffer 對象連接一組字符串,可以用下面的代碼:
var buffer = new StringBuffer (); buffer.append("hello "); buffer.append("world"); var result = buffer.toString();
可用下面的代碼測試 StringBuffer 對象和傳統(tǒng)的字符串連接方法的性能:
var d1 = new Date(); var str = ""; for (var i=0; i < 10000; i++) { str += "text"; } var d2 = new Date(); document.write("Concatenation with plus: " + (d2.getTime() - d1.getTime()) + " milliseconds"); var buffer = new StringBuffer(); d1 = new Date(); for (var i=0; i < 10000; i++) { buffer.append("text"); } var result = buffer.toString(); d2 = new Date(); document.write("<br />Concatenation with StringBuffer: " + (d2.getTime() - d1.getTime()) + " milliseconds");
這段代碼對字符串連接進(jìn)行兩個測試,第一個使用加號,第二個使用 StringBuffer 類。每個操作都連接 10000 個字符串。日期值 d1 和 d2 用于判斷完成操作需要的時間。請注意,創(chuàng)建 Date 對象時,如果沒有參數(shù),賦予對象的是當(dāng)前的日期和時間。要計算連接操作歷經(jīng)多少時間,把日期的毫秒表示(用 getTime() 方法的返回值)相減即可。這是衡量 JavaScript 性能的常見方法。該測試的結(jié)果可以幫助您比較使用 StringBuffer 類與使用加號的效率差異。
更多建議: