1、JavaScript對象的創建方式 在JavaScript中,創建對象的方式包括兩種:對象字面量和使用new表達式。對象字面量是一種靈活方便的書寫方式,例如: 1 2 3 4 5 6 var o1 = { p:”I’m in Object literal”, alertP:function(){ ...
1、JavaScript對象的創建方式
在JavaScript中,創建對象的方式包括兩種:對象字面量和使用new表達式。對象字面量是一種靈活方便的書寫方式,例如:
1 2 3 4 5 6 |
var o1 = {
p:”I’m in Object literal”,
alertP:function(){
alert( this .p);
}
}
|
這樣,就用對象字面量創建了一個對象o1,它具有一個成員變數p以及一個成員方法alertP。這種寫法不需要定義構造函數,因此不在本文的討論範圍之內。這種寫法的缺點是,每創建一個新的對象都需要寫出完整的定義語句,不便於創建大量相同類型的對象,不利於使用繼承等高級特性。
new表達式是配合構造函數使用的,例如new String(“a string”),調用內置的String函數構造了一個字元串對象。下麵我們用構造函數的方式來重新創建一個實現同樣功能的對象,首先是定義構造函數,然後是調用new表達式:
1 2 3 4 5 6 7 |
function CO(){
this .p = “I’m in constructed object”;
this .alertP = function(){
alert( this .p);
}
}
var o2 = newCO();
|
那麼,在使用new操作符來調用一個構造函數的時候,發生了什麼呢?其實很簡單,就發生了四件事:
1 2 3 4 |
var obj ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return obj;
|
第一行,創建一個空對象obj。
第二行,將這個空對象的__proto__成員指向了構造函數對象的prototype成員對象,這是最關鍵的一步,具體細節將在下文描述。
第三行,將構造函數的作用域賦給新對象,因此CA函數中的this指向新對象obj,然後再調用CO函數。於是我們就給obj對象賦值了一個成員變數p,這個成員變數的值是” I’min constructed object”。
第四行,返回新對象obj。當構造函數里包含返回語句時情況比較特殊,這種情況會在下文中說到。
2、正確定義JavaScript構造函數
不同於其它的主流編程語言,JavaScript的構造函數並不是作為類的一個特定方法存在的;當任意一個普通函數用於創建一類對象時,它就被稱作構造函數,或構造器。一個函數要作為一個真正意義上的構造函數,需要滿足下列條件:
(1)在函數內部對新對象(this)的屬性進行設置,通常是添加屬性和方法。
(2)構造函數可以包含返回語句(不推薦),但返回值必須是this,或者其它非對象類型的值。
上文定義的構造函數CO就是一個標準的、簡單的構造函數。下麵例子定義的函數C1返回了一個對象,我們可以使用new表達式來調用它,該表達式可以正確返回一個對象:
1 2 3 4 5 6 7 8 |
function C1(){
var o = {
p:”I’m p in C1”
}
return o;
}
var o1 = new C1();
alert(o1.p); //I’m p in C1
|
但這種方式並不是值得推薦的方式,因為對象o1的原型是函數C1內部定義的對象o的原型,也就是Object.prototype。這種方式相當於執行了正常new表達式的前三步,而在第四步的時候返回了C1函數的返回值。該方式同樣不便於創建大量相同類型的對象,不利於使用繼承等高級特性,並且容易造成混亂,應該摒棄。
一個構造函數在某些情況下完全可以作為普通的功能函數來使用,這是JavaScript靈活性的一個體現。下例定義的C2就是一個“多用途”函數:
1 2 3 4 5 6 7 8 9 10 |
function C2(a, b){
this .p = a + b;
this .alertP = function(){
alert( this .p);
}
return this .p; //此返回語句在C2作為構造函數時沒有意義
}
var c2 = new C2( 2 , 3 );
c2.alertP(); //結果為5
alert(C2( 2 , 3 )); //結果為5
|
該函數既可以用作構造函數來構造一個對象,也可以作為普通的函數來使用。用作普通函數時,它接收兩個參數,並返回兩者的相加的結果。為了代碼的可讀性和可維護性,建議作為構造函數的函數不要摻雜除構造作用以外的代碼;同樣的,一般的功能函數也不要用作構造對象。
3、為什麼要使用構造函數
根據上文的定義,在錶面上看來,構造函數似乎只是對一個新創建的對象進行初始化,增加一些成員變數和方法;然而構造函數的作用遠不止這些。為了說明使用構造函數的意義,我們先來回顧一下前文提到的例子。執行var o2 = new CO();創建對象的時候,發生了四件事情:
1 2 3 4 |
var obj ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return obj;
|
我們說最重要的是第二步,將新生成的對象的__prop__屬性賦值為構造函數的prototype屬性,使得通過構造函數創建的所有對象可以共用相同的原型。這意味著同一個構造函數創建的所有對象都繼承自一個相同的對象,因此它們都是同一個類的對象。
在JavaScript標準中,並沒有__prop__這個屬性,不過它現在已經是一些主流的JavaScript執行環境預設的一個標準屬性,用於指向構造函數的原型。該屬性是預設不可見的,而且在各執行環境中實現的細節不盡相同,例如IE瀏覽器中不存在該屬性。我們只要知道JavaScript對象內部存在指向構造函數原型的指針就可以了,這個指針是在調用new表達式的時候自動賦值的,並且我們不應該去修改它。
在構造對象的四個步驟中,我們可以看到,除第二步以外,別的步驟我們無須藉助new表達式去實現,因此new表達式不僅僅是對這四個步驟的簡化,也是要實現繼承的必經之路。
4、容易混淆的地方
關於JavaScript的構造函數,有一個容易混淆的地方,那就是原型的constructor屬性。在JavaScript中,每一個函數都有預設的原型對象屬性prototype,該對象預設包含了兩個成員屬性:constructor和__proto__。關於原型的細節就不在本文贅述了,我們現在關心的是這個constructor屬性。
按照面向對象的習慣性思維,我們說構造函數相當於“類”的定義,從而可能會認為constructor屬性就是該類實際意義上的構造函數,在new表達式創建一個對象的時候,會直接調用constructor來初始化對象,那就大錯特錯了。new表達式執行的實際過程已經在上文中介紹過了(四個步驟),其中用於初始化對象的是第三步,調用的初始化函數正是“類函數”本身,而不是constructor。如果沒有考慮過這個問題,這一點可能不太好理解,那就讓我們舉個例子來說明一下吧:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function C3(a, b){
this .p = a + b;
this .alertP = function(){
alert( this .p);
}
}
//我們定義一個函數來覆蓋C3原型中的constructor,試圖改變屬性p的值
function fake(){
this .p = 100 ;
}
C3.prototype.constructor = fake; //覆蓋C3原型中的constructor
var c3 = new C3( 2 , 3 );
c3.alertP(); //結果仍然為5
|
上述代碼手動改變了C3原型中的constructor函數,然而卻沒有對c3對象的創建產生實質的影響,可見在new表達式中,起初始化對象作用的只能是構造函數本身。那麼constructor屬性的作用是什麼呢?一般來說,我們可以使用constructor屬性來測試對象的類型:
1 2 |
var myArray = [ 1 , 2 , 3 ];
(myArray.constructor == Array); // true
|
這招對於簡單的對象是管用的,涉及到繼承或者跨視窗等複雜情況時,可能就沒那麼靈光了:
1 2 3 4 5 6 7 |
function f() { this .foo = 1 ;}
function s() { this .bar = 2 ; }
s.prototype = new f(); // s繼承自f
var son = new s(); // 用構造函數s創建一個子類對象
(son.constructor == s); // false
(son.constructor == f); // true
|
這樣的結果可能跟你的預期不相一致,所以使用constructor屬性的時候一定要小心,或者乾脆不要用它。