構造函數的定義
ECMAScript中的構造函數可用來創建特定類型的對象。像Object和Array這樣的原生構造函數,在運行時會自動出現在執行環境中。此外,也可以創建自定義的構造函數,從而定義自定義對象類型的屬性和方法。
構造函數創建對象
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個例子中,Person()函數取代了createPerson()函數。我們注意到,Person()中的代碼
除了與createPerson()中相同的部分外,還存在以下不同之處:
?沒有顯式地創建對象;
?直接將屬性和方法賦給了this對象;
?沒有return語句。
此外,還應該注意到函數名Person使用的是大寫字母P。按照慣例,構造函數始終都應該以一個
大寫字母開頭,而非構造函數則應該以一個小寫字母開頭。這個做法借鑒自其他OO語言,主要是為了
區別于ECMAScript中的其他函數;因為構造函數本身也是函數,只不過可以用來創建對象而已。
要創建Person的新實例,必須使用new操作符。以這種方式調用構造函數實際上會經歷以下4個步驟:
(1)創建一個新對象;
(2)將構造函數的作用域賦給新對象(因此this就指向了這個新對象);
(3)執行構造函數中的代碼(為這個新對象添加屬性);
(4)返回新對象。
在前面例子的最后,person1和person2分別保存著Person的一個不同的實例。這兩個對象都
有一個constructor(構造函數)屬性,該屬性指向Person,如下所示。
alert(person1.constructor == Person); //true
alert(person2.constructor == Person); //true
對象的constructor屬性最初是用來標識對象類型的。但是,提到檢測對象類型,還是instanceof操作符要更可靠一些。我們在這個例子中創建的所有對象既是Object的實例,同時也是Person的實例,這一點通過instanceof操作符可以得到驗證。
alert(person1 instanceof Object);? //true
alert(person1 instanceof Person);? //true
alert(person2 instanceof Object);? //true
alert(person2 instanceof Person);? //true
創建自定義的構造函數意味著將來可以將它的實例標識為一種特定的類型;而這正是構造函數模式勝過工廠模式的地方。在這個例子中,person1和person2之所以同時是Object的實例,是因為所有對象均繼承自Object。
構造函數和普通函數的區別
構造函數與其他函數的唯一區別,就在于調用它們的方式不同。不過,構造函數畢竟也是函數,不存在定義構造函數的特殊語法。任何函數,只要通過new操作符來調用,那它就可以作為構造函數;而任何函數,如果不通過new操作符來調用,那它跟普通函數也不會有什么兩樣。例如,前面例子中定義的Person()函數可以通過下列任何一種方式來調用。
例如:
//當作構造函數使用
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName(); //"Nicholas"
//作為普通函數調用
Person("Greg", 27, "Doctor"); //添加到window
window.sayName(); //"Greg"
//在另一個對象的作用域中調用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");o.sayName(); //"Kristen"
這個例子中的前兩行代碼展示了構造函數的典型用法,即使用new操作符來創建一個新對象。接下來的兩行代碼展示了不使用new操作符調用Person()會出現什么結果:屬性和方法都被添加給window對象了。有讀者可能還記得,當在全局作用域中調用一個函數時,this對象總是指向Global對象(在瀏覽器中就是window對象)。因此,在調用完函數之后,可以通過window對象來調用sayName()方法,并且還返回了"Greg"。最后,也可以使用call()(或者apply())在某個特殊對象的作用域中調用Person()函數。這里是在對象o的作用域中調用的,因此調用后o就擁有了所有屬性和sayName()方法。
構造函數的問題
構造函數模式雖然好用,但也并非沒有缺點。使用構造函數的主要問題,就是每個方法都要在每個實例上重新創建一遍。在前面的例子中,person1和person2都有一個名為sayName()的方法,但那兩個方法不是同一個Function的實例。不要忘了——ECMAScript中的函數是對象,因此每定義一個函數,也就是實例化了一個對象。從邏輯角度講,此時的構造函數也可以這樣定義。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("alert(this.name)"); //與聲明函數在邏輯上是等價的}
從這個角度上來看構造函數,更容易明白每個Person實例都包含一個不同的Function實例(以顯示name屬性)的本質。說明白些,以這種方式創建函數,會導致不同的作用域鏈和標識符解析,但創建Function新實例的機制仍然是相同的。因此,不同實例上的同名函數是不相等的,以下代碼可以證明這一點。
alert(person1.sayName == person2.sayName);? //false
然而,創建兩個完成同樣任務的Function實例的確沒有必要;況且有this對象在,根本不用在執行代碼前就把函數綁定到特定對象上面。因此,大可像下面這樣,通過把函數定義轉移到構造函數外部來解決這個問題。
例子:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName(){
alert(this.name);
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
在這個例子中,我們把sayName()函數的定義轉移到了構造函數外部。而在構造函數內部,我們將sayName屬性設置成等于全局的sayName函數。這樣一來,由于sayName包含的是一個指向函數的指針,因此person1和person2對象就共享了在全局作用域中定義的同一個sayName()函數。這樣做確實解決了兩個函數做同一件事的問題,可是新問題又來了:在全局作用域中定義的函數實際上只能被某個對象調用,這讓全局作用域有點名不副實。而更讓人無法接受的是:如果對象需要定義很多方法,那么就要定義很多個全局函數,于是我們這個自定義的引用類型就絲毫沒有封裝性可言了。好在,這些問題可以通過使用原型模式來解決。