建構式(Constructor)



如果你有以下建立物件的需求:
var p1 = { name : 'Justin', age : 35};
var p2 = { name : 'Momor', age : 32};
var p3 = { name : 'Hamimi', age : 2};

這些物件在建立時,具有相同的特性名稱,只不過特性值不同,其實你可以定義一個函式:
function Person(name, age) {
    this.name = name;
    this.age = age;
}

接著如下呼叫,就可以有相同的效果:
var p1 = new Person('Justin', 35);
var p2 = new Person('Momor', 32);
var p3 = new Person('Hamimi', 2);

像Person這樣的函式,接在new之後使用時,俗稱為建構式(Constructor),通常對從類別為基礎的語言過來的人,也會說這就像是一個類別(Class),不過這只是比擬,實際上當然與類別有所差別。

實際上你使用new運算子後接上一個函式時,相當於作這樣的動作(一部份是這樣,不過還有別的細節,之後再談):
js> function Person(name, age) {
  >     this.name = name;
  >     this.age = age;
  >     this.toString = function() {
  >         return '[' + this.name + ', ' + this.age + ']';
  >     };
  > }
js> var p = {};
js> Person.call(p, 'Justin', 35);
js> p
[Justin, 35]
js>


這也說明了,為什麼使用new接上函式,傳回的物件會有name與age,因為Person中,this所參考的就是p所參考的物件,所以在this上新增特性,就相當於在p所參考物件上新增特性。

一個函式作為建構式使用時,基本上無需撰寫return,如果建構式有傳回值,那傳回值就會被當作最後名稱所參考的值。例如:
js> function Person(name, age)  {
  >     return [];
  > }
js> var o = new Person();
js> o instanceof Person;
false
js> o instanceof Array;
true
js>


所以要以比擬的方式來說,new之後接上建構式,預設相當於這樣:
js> function Person(name, age) {
  >     this.name = name;
  >     this.age = age;
  >     this.toString = function() {
  >         return '[' + this.name + ', ' + this.age + ']';
  >     };
  >     return this;
  > }
js> var p = {};
js> p = Person.call(p, 'Justin', 35);
[Justin, 35]
js>


每個透過new建構的物件,都會有個constructor特性,參考至當初建構它的函式。例如:
js> function Person() {}
js> var p = new Person();
js> p.constructor == Person
true
js>


這可以作為物件類型的參考依據,不過要注意的是,constructor是可以修改的。

由於透過建構式所建立的物件,所有的特性都是直接新增在物件上,也因此可以直接透過 . 運算子加以存取。例如:
js> function Person(name, age) {
  >     this.name = name;
  >     this.age = age;
  > }
js> var p = new Person('Justin', 35);
js> p.name;
Justin
js> p.age;
35
js>


對熟悉物件導向私有性基本觀念的人來說,可能覺得這不安全,這相當於在物件導向觀念中,每個類別成員都是公開成員的意味。JavaScript本身並不支援物件導向公開、私用性等觀念,如果你想模擬,則可以如下:
js> function Person(name, age) {
  >     this.getName = function() {
  >         return name;
  >     };
  >     this.age = age;
  > }
js> var p = new Person('Justin', 35);
js> print(p.name);
undefined
js> print(p.getName());
Justin
js> print(p.age);
35
js>


以上假設的是,name不可以被設定,但可以透過getName()來取得,之所以會有這樣的效果,其實就是 閉包 的作用。上例中,在物件上新增了getName特性,參考至一個函式,該函式形成閉包綁定了參數name,參數也就是區域變數,並非物件上的特性,所以無法透過 . 運算子取得,因此模擬了私用性。

由於閉包綁定的是變數本身,所以也可以如下,在設定值(或取得值)時予以保護:

js> function Account() {
  >     var balance = 0;
  >     this.getBalance = function() {
  >         return balance;
  >     };
  >     this.setBalance = function(money) {
  >         if(money < 0) {
  >             throw new Error('can\'t set negative balance.');
  >         }
  >         balance = money;
  >     };
  > }
js> var acct = new Account();
js> acct.getBalance();
0
js> acct.setBalance(1000);
js> acct.getBalance();
1000
js> acct.setBalance(-1000);
js: "<stdin>", line 113: exception from uncaught JavaScript throw: Error: can't
set negative balance.
        at <stdin>:113
        at <stdin>:111

js>