隱藏諸多細節的建構式

July 26, 2022

如果你有以下建立物件的需求:

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)。

new 一個實例

實際上使用 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 特性,參考至當初建構它的函式,這是因為函式本身的 prototype 上會有個 constructor,指向函式本身。例如:

function Person() {}
var p = new Person();
console.log(p.constructor === Person);                  // true
console.log(Person.prototype.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

在〈物件特性 API〉談過特性描述器,JavaScript 中直接對物件新增特性,writableenumerableconfigurable 預設都是 true,也就是說,當特性本身其實是個方法時,也會被 for..in 列舉,然而,通常使用 for..in 列舉特性時,並不希望把方法也列舉出來,如果你在意這個,記得透過 Object.defineProperties 設定相關特性的 enumerablefalse

建構式還有一些細節需要瞭解,這會在下一篇文件中繼續討論。

分享到 LinkedIn 分享到 Facebook 分享到 Twitter