JavaScript 語言核心(14)隱藏諸多細節的建構式



如果你有以下建立物件的需求:
function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

var p1 = {
    name     : 'Justin', 
    age      : 35,
    toString : toString
};

var p2 = {
    name     : 'Monica', 
    age      : 32,
    toString : toString
};

var p3 = {
    name     : 'Irene', 
    age      : 2,
    toString : toString
};

console.log(p1.toString());  // [Justin,35] 
console.log(p2.toString());  // [Monica,32] 
console.log(p3.toString());  // [Irene,2]
這些物件在建立時,具有相同的特性名稱,只不過特性值不同,其實你如下定義 Person 函式:
function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = toString;
}

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

console.log(p1.toString());  // [Justin,35] 
console.log(p2.toString());  // [Monica,32] 
console.log(p3.toString());  // [Irene,2]
Person 這樣的函式,接在 new 之後使用時,俗稱為建構式(Constructor),通常對從類別為基礎的語言過來的人,也會說這就像是一個類別(Class),不過這只是比擬,實際上與它並不是類別。
實際上使用 new 運算子後接上一個函式時,一部份是在作以下的動作:
function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.toString = toString;
}

var p = {};
Person.call(p, 'Justin', 35);

console.log(p.toString());  // [Justin,35]
這也說明了,為什麼使用 new 接上函式,傳回的物件會有 nameage,因為 Person 中,this 參考的就是 p 所參考的物件,所以在 this 上新增特性,就相當於在 p 所參考物件上新增特性。
說是一部份作了這些動作,不過還有別的細節,像是原型繼承以及 constructor 特性的指定等,不然的話,你其實大可以如下定義就好了:
function toString() {
    return '[' + this.name + ',' + this.age + ']';
}

function person(name, age) {
    return {
        name     : name,
        age      : age,
        toString : toString
    };
}

var p = person('Justin', 35);

console.log(p.toString());  // [Justin,35]
原型繼承會在另一篇文章中說明,稍後則就會看到 constructor 的說明。
一個函式作為建構式使用時,基本上無需撰寫 return,如果建構式有傳回值,那傳回值就會被當作 new XXX(...) 的結果。例如:
function Nobody()  {
}

function Person(name, age)  {
    return [];
}

var n = new Nobody();
var p = new Person();

console.log(n instanceof Nobody);  // true
console.log(p instanceof Person);  // false
console.log(p instanceof Array);   // true
instanceof 可用來測試物件是否由經由某個建構式 new 出來,由於實際上 Person 中定義了 return []new Person() 傳回的是 [],因此 instanceof 測試結果並不是 Person 建構的實例。
每個透過 new 建構的物件,都會有個 constructor 特性,參考至當初建構它的函式。例如:
function Person() {}
var p = new Person();
console.log(p.constructor == Person);  // true
雖然這可以作為判斷物件類型的參考依據之一,不過要注意的是,constructor 是可以修改的,因而並不可靠,instanceof 也不是使用 constructor 來判斷物件是否為某建構式的實例,而是根據物件的原型物件,這之後會有另一篇文章來探討。
由於透過建構式所建立的物件,所有的特性都是直接新增在物件上,也因此可以直接透過 . 運算子加以存取。例如:
function Person(name, age) {
    this.name = name;
    this.age = age;
}

var p = new Person('Justin', 35);

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

    this.age = age;
}

var p = new Person('Justin', 35);

console.log(p.name);       // undefined
console.log(p.getName());  // Justin
console.log(p.age);        // 35
以上假設的是,name 不可以被設定,但可以透過 getName 來取得,之所以會有這樣的效果,其實就是 Closure 的作用。上例中,在物件上新增了 getName 特性,參考至一個函式,該函式形成 Closure 綁定了參數 name,參數也就是區域變數,並非物件上的特性,所以無法透過 . 運算子取得,因此模擬了私用特性。
由於 Closure 綁定的是變數本身,所以也可以如下,在設定值(或取得值)時予以保護:
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;
    };
}

var acct = new Account();

console.log(acct.getBalance());   // 0

acct.setBalance(1000);
console.log(acct.getBalance());   // 1000

acct.setBalance(-1000);           // Error: can't set negative balance
建構式還有一些細節需要瞭解,這會在下一篇文章中繼續討論。