最近更新: 2011-12-05

產生新類別的類別

介紹 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 的設計者,這是必要知識。

相關文章
樂多舊網址: http://blog.roodo.com/rocksaying/archives/18294075.html