深入理解js構造函數

JavaScript對象的創建方式

在JavaScript中,創建對象的方式包括兩種:對象字面量和使用new表達式。對象字面量是一種靈活方便的書寫方式,例如:

var o1 = {
    p:”I’m in Object literal”,
    alertP:function(){
        alert(this.p);
    }
}

這樣,就用對象字面量創建瞭一個對象o1,它具有一個成員變量p以及一個成員方法alertP。這種寫法不需要定義構造函數,因此不在本文的討論范圍之內。這種寫法的缺點是,每創建一個新的對象都需要寫出完整的定義語句,不便於創建大量相同類型的對象,不利於使用繼承等高級特性。

new表達式是配合構造函數使用的,例如new String(“a string”),調用內置的String函數構造瞭一個字符串對象。下面我們用構造函數的方式來重新創建一個實現同樣功能的對象,首先是定義構造函數,然後是調用new表達式:

function CO(){
    this.p = “I’m in constructed object”;
    this.alertP = function(){
        alert(this.p);
    }
}
var o2 = newCO();

那麼,在使用new操作符來調用一個構造函數的時候,發生瞭什麼呢?其實很簡單,就發生瞭四件事:

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。當構造函數裡包含返回語句時情況比較特殊,這種情況會在下文中說到。

正確定義JavaScript構造函數

不同於其它的主流編程語言,JavaScript的構造函數並不是作為類的一個特定方法存在的;當任意一個普通函數用於創建一類對象時,它就被稱作構造函數,或構造器。一個函數要作為一個真正意義上的構造函數,需要滿足下列條件:

1、 在函數內部對新對象(this)的屬性進行設置,通常是添加屬性和方法。

2、 構造函數可以包含返回語句(不推薦),但返回值必須是this,或者其它非對象類型的值。

上文定義的構造函數CO就是一個標準的、簡單的構造函數。下面例子定義的函數C1返回瞭一個對象,我們可以使用new表達式來調用它,該表達式可以正確返回一個對象:

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就是一個“多用途”函數:

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

該函數既可以用作構造函數來構造一個對象,也可以作為普通的函數來使用。用作普通函數時,它接收兩個參數,並返回兩者的相加的結果。為瞭代碼的可讀性和可維護性,建議作為構造函數的函數不要摻雜除構造作用以外的代碼;同樣的,一般的功能函數也不要用作構造對象。

為什麼要使用構造函數

根據上文的定義,在表面上看來,構造函數似乎隻是對一個新創建的對象進行初始化,增加一些成員變量和方法;然而構造函數的作用遠不止這些。為瞭說明使用構造函數的意義,我們先來回顧一下前文提到的例子。執行var o2 = new CO();創建對象的時候,發生瞭四件事情:

var obj  ={};
obj.__proto__ = CO.prototype;
CO.call(obj);
return obj;

我們說最重要的是第二步,將新生成的對象的__prop__屬性賦值為構造函數的prototype屬性,使得通過構造函數創建的所有對象可以共享相同的原型。這意味著同一個構造函數創建的所有對象都繼承自一個相同的對象,因此它們都是同一個類的對象。關於原型(prototype)和繼承的細節,筆者會再另一篇文章中深入說明。

在JavaScript標準中,並沒有__prop__這個屬性,不過它現在已經是一些主流的JavaScript執行環境默認的一個標準屬性,用於指向構造函數的原型。該屬性是默認不可見的,而且在各執行環境中實現的細節不盡相同,例如IE瀏覽器中不存在該屬性。我們隻要知道JavaScript對象內部存在指向構造函數原型的指針就可以瞭,這個指針是在調用new表達式的時候自動賦值的,並且我們不應該去修改它。

在構造對象的四個步驟中,我們可以看到,除第二步以外,別的步驟我們無須借助new表達式去實現,因此new表達式不僅僅是對這四個步驟的簡化,也是要實現繼承的必經之路。

容易混淆的地方

關於JavaScript的構造函數,有一個容易混淆的地方,那就是原型的constructor屬性。在JavaScript中,每一個函數都有默認的原型對象屬性prototype,該對象默認包含瞭兩個成員屬性:constructor和__proto__。關於原型的細節就不在本文贅述瞭,我們現在關心的是這個constructor屬性。

按照面向對象的習慣性思維,我們說構造函數相當於“類”的定義,從而可能會認為constructor屬性就是該類實際意義上的構造函數,在new表達式創建一個對象的時候,會直接調用constructor來初始化對象,那就大錯特錯瞭。new表達式執行的實際過程已經在上文中介紹過瞭(四個步驟),其中用於初始化對象的是第三步,調用的初始化函數正是“類函數”本身,而不是constructor。如果沒有考慮過這個問題,這一點可能不太好理解,那就讓我們舉個例子來說明一下吧:

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屬性來測試對象的類型:

var myArray = [1,2,3];
(myArray.constructor == Array); // true

這招對於簡單的對象是管用的,涉及到繼承或者跨窗口等復雜情況時,可能就沒那麼靈光瞭:

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屬性的時候一定要小心,或者幹脆不要用它。

發佈留言