構造對象
好了,接下我們來討論一下對象的另一種創建方法。
除JSON外,在JavaScript中我們可以使用new操作符結合一個函數的形式來創建對象。例如:
function MyFunc() {}; // 定義一個空函數
var anObj = new MyFunc(); // 使用new操作符,借助MyFun函數,就創建了一個對象
JavaScript的這種創建對象的方式可真有意思,如何去理解這種寫法呢?
其實,可以把上面的代碼改寫成這種等價形式:
function MyFunc(){};
var anObj = {}; // 創建一個對象
MyFunc.call(anObj); // 將anObj對象作為this指針調用MyFunc函數
我們就可以這樣理解,JavaScript先用new操作符創建了一個對象,緊接著就將這個對象作為this參數調用了后面的函數。其實,JavaScript內部就是這么做的,而且任何函數都可以被這樣調用!但從 “anObj = new MyFunc()” 這種形式,我們又看到一個熟悉的身影,C++和C#不就是這樣創建對象的嗎?原來,條條大路通靈山,殊途同歸啊!
君看到此處也許會想,我們為什么不可以把這個MyFunc當作構造函數呢?恭喜你,答對了!JavaScript也是這么想的!請看下面的代碼:
1 function Person(name) // 帶參數的構造函數
2 {
3 this .name = name; // 將參數值賦給給this對象的屬性
4 this .SayHello = function () {alert( " Hello, I'm " + this .name);}; // 給this對象定義一個SayHello方法。
5 };
6
7 function Employee(name, salary) // 子構造函數
8 {
9 Person.call( this , name); // 將this傳給父構造函數
10 this .salary = salary; // 設置一個this的salary屬性
11 this .ShowMeTheMoney = function () {alert( this .name + " $ " + this .salary);}; // 添加ShowMeTheMoney方法。
12 };
13
14 var BillGates = new Person( " Bill Gates " ); // 用Person構造函數創建BillGates對象
15 var SteveJobs = new Employee( " Steve Jobs " , 1234 ); // 用Empolyee構造函數創建SteveJobs對象
16
17 BillGates.SayHello(); // 顯示:I'm Bill Gates
18 SteveJobs.SayHello(); // 顯示:I'm Steve Jobs
19 SteveJobs.ShowMeTheMoney(); // 顯示:Steve Jobs $1234
20
21 alert(BillGates.constructor == Person); // 顯示:true
22 alert(SteveJobs.constructor == Employee); // 顯示:true
23
24 alert(BillGates.SayHello == SteveJobs.SayHello); // 顯示:false
這段代碼表明,函數不但可以當作構造函數,而且還可以帶參數,還可以為對象添加成員和方法。其中的第9行,Employee構造函數又將自己接收的 this作為參數調用Person構造函數,這就是相當于調用基類的構造函數。第21、22行還表明這樣一個意思:BillGates是由Person構造的,而SteveJobs是由Employee構造的。對象內置的constructor屬性還指明了構造對象所用的具體函數!
其實,如果你愿意把函數當作“類”的話,她就是“類”,因為她本來就有“類”的那些特征。難道不是嗎?她生出的兒子各個都有相同的特征,而且構造函數也與類同名嘛!
但要注意的是,用構造函數操作this對象創建出來的每一個對象,不但具有各自的成員數據,而且還具有各自的方法數據。換句話說,方法的代碼體(體現函數邏輯的數據)在每一個對象中都存在一個副本。盡管每一個代碼副本的邏輯是相同的,但對象們確實是各自保存了一份代碼體。上例中的最后一句說明了這一實事,這也解釋了JavaScript中的函數就是對象的概念。
同一類的對象各自有一份方法代碼顯然是一種浪費。在傳統的對象語言中,方法函數并不象JavaScript那樣是個對象概念。即使也有象函數指針、方法指針或委托那樣的變化形式,但其實質也是對同一份代碼的引用。一般的對象語言很難遇到這種情況。
不過,JavaScript語言有大的靈活性。我們可以先定義一份唯一的方法函數體,并在構造this對象時使用這唯一的函數對象作為其方法,就能共享方法邏輯。例如:
function SayHello() // 先定義一份SayHello函數代碼
{
alert( " Hello, I'm " + this .name);
};
function Person(name) // 帶參數的構造函數
{
this .name = name; // 將參數值賦給給this對象的屬性
this .SayHello = SayHello; // 給this對象SayHello方法賦值為前面那份SayHello代碼。
};
var BillGates = new Person( " Bill Gates " ); // 創建BillGates對象
var SteveJobs = new Person( " Steve Jobs " ); // 創建SteveJobs對象
alert(BillGates.SayHello == SteveJobs.SayHello); // 顯示:true
其中,最后一行的輸出結果表明兩個對象確實共享了一個函數對象。雖然,這段程序達到了共享了一份方法代碼的目的,但卻不怎么優雅。因為,定義 SayHello方法時反映不出其與Person類的關系。“優雅”這個詞用來形容代碼,也不知道是誰先提出來的。不過,這個詞反映了程序員已經從追求代碼的正確、高效、可靠和易讀等基礎上,向著追求代碼的美觀感覺和藝術境界的層次發展,程序人生又多了些浪漫色彩。
顯然,JavaScript早想到了這一問題,她的設計者們為此提供了一個有趣的prototype概念。
初看原型
prototype源自法語,軟件界的標準翻譯為“原型”,代表事物的初始形態,也含有模型和樣板的意義。JavaScript中的prototype概念恰如其分地反映了這個詞的內含,我們不能將其理解為C++的prototype那種預先聲明的概念。
JavaScript的所有function類型的對象都有一個prototype屬性。這個prototype屬性本身又是一個object類型的對象,因此我們也可以給這個prototype對象添加任意的屬性和方法。既然prototype是對象的“原型”,那么由該函數構造出來的對象應該都會具有這個“原型”的特性。事實上,在構造函數的prototype上定義的所有屬性和方法,都是可以通過其構造的對象直接訪問和調用的。也可以這么說,prototype提供了一群同類對象共享屬性和方法的機制。
我們先來看看下面的代碼:
function Person(name)
{
this .name = name; // 設置對象屬性,每個對象各自一份屬性數據
};
Person.prototype.SayHello = function () // 給Person函數的prototype添加SayHello方法。
{
alert( " Hello, I'm " + this .name);
}
var BillGates = new Person( " Bill Gates " ); // 創建BillGates對象
var SteveJobs = new Person( " Steve Jobs " ); // 創建SteveJobs對象
BillGates.SayHello(); // 通過BillGates對象直接調用到SayHello方法
SteveJobs.SayHello(); // 通過SteveJobs對象直接調用到SayHello方法
alert(BillGates.SayHello == SteveJobs.SayHello); // 因為兩個對象是共享prototype的SayHello,所以顯示:true
程序運行的結果表明,構造函數的prototype上定義的方法確實可以通過對象直接調用到,而且代碼是共享的。顯然,把方法設置到prototype的寫法顯得優雅多了,盡管調用形式沒有變,但邏輯上卻體現了方法與類的關系,相對前面的寫法,更容易理解和組織代碼。
那么,對于多層次類型的構造函數情況又如何呢?
我們再來看下面的代碼:
1 function Person(name) // 基類構造函數
2 {
3 this .name = name;
4 };
5
6 Person.prototype.SayHello = function () // 給基類構造函數的prototype添加方法
7 {
8 alert( " Hello, I'm " + this .name);
9 };
10
11 function Employee(name, salary) // 子類構造函數
12 {
13 Person.call( this , name); // 調用基類構造函數
14 this .salary = salary;
15 };
16
17 Employee.prototype = new Person(); // 建一個基類的對象作為子類原型的原型,這里很有意思
18
19 Employee.prototype.ShowMeTheMoney = function () // 給子類添構造函數的prototype添加方法
20 {
21 alert( this .name + " $ " + this .salary);
22 };
23
24 var BillGates = new Person( " Bill Gates " ); // 創建基類Person的BillGates對象
25 var SteveJobs = new Employee( " Steve Jobs " , 1234 ); // 創建子類Employee的SteveJobs對象
26
27 BillGates.SayHello(); // 通過對象直接調用到prototype的方法
28 SteveJobs.SayHello(); // 通過子類對象直接調用基類prototype的方法,關注!
29 SteveJobs.ShowMeTheMoney(); // 通過子類對象直接調用子類prototype的方法
30
31 alert(BillGates.SayHello == SteveJobs.SayHello); // 顯示:true,表明prototype的方法是共享的
這段代碼的第17行,構造了一個基類的對象,并將其設為子類構造函數的prototype,這是很有意思的。這樣做的目的就是為了第28行,通過子類對象也可以直接調用基類prototype的方法。為什么可以這樣呢?
原來,在JavaScript中,prototype不但能讓對象共享自己財富,而且prototype還有尋根問祖的天性,從而使得先輩們的遺產可以代代相傳。當從一個對象那里讀取屬性或調用方法時,如果該對象自身不存在這樣的屬性或方法,就會去自己關聯的prototype對象那里尋找;如果 prototype沒有,又會去prototype自己關聯的前輩prototype那里尋找,直到找到或追溯過程結束為止。
在JavaScript內部,對象的屬性和方法追溯機制是通過所謂的prototype鏈來實現的。當用new操作符構造對象時,也會同時將構造函數的 prototype對象指派給新創建的對象,成為該對象內置的原型對象。對象內置的原型對象應該是對外不可見的,盡管有些瀏覽器(如Firefox)可以讓我們訪問這個內置原型對象,但并不建議這樣做。內置的原型對象本身也是對象,也有自己關聯的原型對象,這樣就形成了所謂的原型鏈。
在原型鏈的最末端,就是Object構造函數prototype屬性指向的那一個原型對象。這個原型對象是所有對象的最老祖先,這個老祖宗實現了諸如 toString等所有對象天生就該具有的方法。其他內置構造函數,如Function, Boolean, String, Date和RegExp等的prototype都是從這個老祖宗傳承下來的,但他們各自又定義了自身的屬性和方法,從而他們的子孫就表現出各自宗族的那些特征。
這不就是“繼承”嗎?是的,這就是“繼承”,是JavaScript特有的“原型繼承”。
“原型繼承”是慈祥而又嚴厲的。原形對象將自己的屬性和方法無私地貢獻給孩子們使用,也并不強迫孩子們必須遵從,允許一些頑皮孩子按自己的興趣和愛好獨立行事。從這點上看,原型對象是一位慈祥的母親。然而,任何一個孩子雖然可以我行我素,但卻不能動原型對象既有的財產,因為那可能會影響到其他孩子的利益。從這一點上看,原型對象又象一位嚴厲的父親。我們來看看下面的代碼就可以理解這個意思了:
function Person(name)
{
this .name = name;
};
Person.prototype.company = " Microsoft " ; // 原型的屬性
Person.prototype.SayHello = function () // 原型的方法
{
alert( " Hello, I'm " + this .name + " of " + this .company);
};
var BillGates = new Person( " Bill Gates " );
BillGates.SayHello(); // 由于繼承了原型的東西,規規矩矩輸出:Hello, I'm Bill Gates
var SteveJobs = new Person( " Steve Jobs " );
SteveJobs.company = " Apple " ; // 設置自己的company屬性,掩蓋了原型的company屬性
SteveJobs.SayHello = function () // 實現了自己的SayHello方法,掩蓋了原型的SayHello方法
{
alert( " Hi, " + this .name + " like " + this .company + " , ha ha ha " );
};
SteveJobs.SayHello(); // 都是自己覆蓋的屬性和方法,輸出:Hi, Steve Jobs like Apple, ha ha ha
BillGates.SayHello(); // SteveJobs的覆蓋沒有影響原型對象,BillGates還是按老樣子輸出
對象可以掩蓋原型對象的那些屬性和方法,一個構造函數原型對象也可以掩蓋上層構造函數原型對象既有的屬性和方法。這種掩蓋其實只是在對象自己身上創建了新的屬性和方法,只不過這些屬性和方法與原型對象的那些同名而已。JavaScript就是用這簡單的掩蓋機制實現了對象的“多態”性,與靜態對象語言的虛函數和重載(override)概念不謀而合。
然而,比靜態對象語言更神奇的是,我們可以隨時給原型對象動態添加新的屬性和方法,從而動態地擴展基類的功能特性。這在靜態對象語言中是很難想象的。我們來看下面的代碼:
function Person(name)
{
this .name = name;
};
Person.prototype.SayHello = function () // 建立對象前定義的方法
{
alert( " Hello, I'm " + this .name);
};
var BillGates = new Person( " Bill Gates " ); // 建立對象
BillGates.SayHello();
Person.prototype.Retire = function () // 建立對象后再動態擴展原型的方法
{
alert( " Poor " + this .name + " , bye bye! " );
};
BillGates.Retire(); // 動態擴展的方法即可被先前建立的對象立即調用
阿彌佗佛,原型繼承竟然可以玩出有這樣的法術!
原著:李戰(leadzen) http://www.cnblogs.com/leadzen/archive/2008/02/25/1073404.html