隱藏諸多細節的建構式
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
接上函式,傳回的物件會有 name
與 age
,因為 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 中直接對物件新增特性,writable
、enumerable
、configurable
預設都是 true
,也就是說,當特性本身其實是個方法時,也會被 for..in
列舉,然而,通常使用 for..in
列舉特性時,並不希望把方法也列舉出來,如果你在意這個,記得透過 Object.defineProperties
設定相關特性的 enumerable
為 false
。
建構式還有一些細節需要瞭解,這會在下一篇文件中繼續討論。