產生新類別的類別
介紹 JavaScript 關於函數與建構者的基礎知識。理解此一基礎,將建構者內容參數化。 透過參數化的技巧,實作一個產生新類別的類別。
JavaScript 定義類別的基本方式
JavaScript 沒有類別定義的關鍵字。"定義類別"這句話在 JavaScript 中的意義, 等於是定義一個新的建構者(Constructor)。
JavaScript中的「建構者」是什麼?
答案是一個具有建立與賦予初值給個體之能力的函數實體。
A constructor is a Function object that creates and initialises objects.
簡單說,建構者就是一個函數。
參考「JavaScript的中介編程與反射能力示範」。
以下 JavaScript 程式碼示範如何定義建構者,亦可理解為定義類別。
// 定義一個新的建構者,名稱為 Class1;
// 亦可理解為定義一個新的類別,名稱為 Class1。
function Class1() {
}
/* public members. */
Class1.prototype =
{
foo: function() {
print("foo...");
}
}
var c1 = new Class1(); // allocate an instance of Class1.
c1.foo();
配合 JavaScript 的用語習慣,在本文中主要用「建構者」一詞,而不用「類別」。
JavaScript 表達函數內文的方式
JavaScript 定義函數的語法,有兩種表達函數內文的方式。
第一種方式使用 { }
括起函數內文,這是我們熟悉的表達方式。
第二種方式則是用字串表達函數內文,詳見 ECMA-262 規格書第 15.3.2.1 節。
The last argument specifies the body (executable code) of a function; any preceding arguments specify formal parameters. 15.3.2.1 new Function (p1, p2, ... , pn, body)
在 meta-programming 的場合中,我們通常需要採用第二種方式。 以下程式碼所表達的內容等義。
// 第1種,以 {} 括起函數內文。
function Class2() {
this.title = "";
this.value = 0;
}
// 第2種,以字串表達函數內文。
var Class3 = new Function(' \
this.title=""; \
this.value = 0;');
建構者內容的參數化
首先,我們先複習一次常規的、一般的建構者定義內容。 class11.js 定義了一個名為 Class11 的建構者,Class11.prototype 指派其公開成員。
function Class11() {
this.value = 0;
}
Class11.prototype = {
inc: function(v) {
this.value += v;
return this;
},
val: function(v) {
if (v != undefined) { // setter
this.value = v;
return this;
}
else { // getter
return this.value;
}
}
}
var o1 = new Class11();
print(o1.val(3).inc(5).val());
接著,我們把建構者的函數內文改成字串表達,並把定義公開成員的程式碼拆開撰寫。 改成下列內容。
// 將函數內文改成字串表達。
var func_body = ' \
this.value = 0; \
';
// 把 prototype 成員的程式碼段落分開。
var proto = {
inc: function(v) {
this.value += v;
return this;
},
val: function(v) {
if (v != undefined) { // setter
this.value = v;
return this;
}
else { // getter
return this.value;
}
}
};
// 以下的表達方式,可以將類別內容參數化。
var Class11 = new Function(func_body);
Class11.prototype = proto;
var o1 = new Class11();
print(o1.val(3).inc(5).val());
從上例的改寫程式碼中,我們可以看出建構者的函數內文與其公開成員的內容,都可以變成一個參數。 結合以上的建構者內容參數化的演示,可以寫出下列專門用來產生新的類別的建構者。 另一種說法是: 它是一個可以產生匿名類別的類別。 我個人則習慣說是「配置一個未分類個體」。
function UnclassifiedObject(construct, proto) {
var func_body = "";
if (construct) {
func_body = construct.toString();
func_body = func_body.substring(
func_body.indexOf('{')+1,
func_body.lastIndexOf('}'));
// strip 'function() {' and '}'.
}
var c = new Function(func_body);
// var c = construct; // ERROR! it will take a reference to construct.
if (proto)
c.prototype = proto;
return c;
}
UnclassifiedObject 的用例
UnclassifiedObject 的回傳結果是一個新的類別。根據是否配合 new 運算子而有兩種使用途徑。
第一、將 UnclassifiedObject 產生的新類別指派一個名稱,使這個新類別變成一般的具名類別來用。
// 配置一個類別,並指派其名稱為 'NewClass'。
var NewClass = UnclassifiedObject(
/*construct*/
function() {
this.value = 0;
},
proto
);
var o1 = new NewClass();
print(o1.inc(3).val());
第二、配合 new 運算子,直接配置一個未分類個體。
// 注意運算順序!
// 先調用 UnclassifiedObject() 回傳一個新的類別,
// 此回傳值再接上 new 運算子後,就產生一個新的未分類個體。
// aka. 匿名類別實體。
var o2 = new (UnclassifiedObject(
/*construct*/
function() {
this.value = 0;
},
proto
));
print(o2.inc(3).val());
大部份的 JavaScript library ,例如 extJS, jQuery 等,事實上都是透過這種機制,為 JavaScript 擴充了類別定義功能。 你或許不再需要自己設計一個類別的類別,但了解它的基礎觀念,總是百利而無害。 對於一位 JavaScript library 的設計者,這是必要知識。