最近更新: 2009-11-09

JavaScript的中介編程與反射能力示範

基於某些原因,我這幾天嘗試分別以 JavaScript, PHP, Ruby (排名按字母順序) 實現同一個簡單的功能,這個功能用了簡單的反射與中介編程技巧。 主要目的是看這些語言在動態型別、中介編程、迭代與反射語法方面的表現。 最後,我會用 Java 語言來實現這個需求,「展現Java語言的特點」。

這篇構想中的文章,愈寫愈長。我想了想,還是按程式語言拆成幾篇,先把要示範的源碼與說明發佈上來。 這是第一篇發佈的,內容是 JavaScript 的實作,示範了兩個基本的中介編程技巧: foreach 和 accessor。

建構者(Constructor)

所有建構者都是個體,但並非每個個體都是建構者。
All constructors are objects, but not all objects are constructors.

ECMAScrpit Language Specification, page 3

所以 Abc = {}; 就只是配置一個單純的個體,而不會產生一個建構者。

那麼建構者從何而來?

建構者是一個具有建立與賦予初值給個體之能力的函數實體。
A constructor is a Function object that creates and initialises objects.

ECMAScript Language Specification, page 4

和 Java 的半調子 OOP 世界不同,在 JavaScrpit 的世界中,函數本身就是一個個體。 當我們定義一個函數,並且函數內部執行了對自己增加成員並賦予成員初值的動作時, 這個函數實體就被視為一個建構者。用 Java 說法,這個東西是一個類別。

在 JavaScript/ECMAScript 裡,配置建構者的寫法有兩種。 如下所示:

1.配置一個名稱叫 F 的建構者。
function F() {
        //增加成員並賦予初值
        this.a = null; //公開成員 a
        var b = null;  //私有成員 b
    }
2.配置一個新函數,指派給變量 F
F = new Function() {
        //增加成員並賦予初值
        this.a = null; //公開成員 a
        var b = null;  //私有成員 b
    };

這兩種寫法等義。

Prototype 與可繼承方法

ECMAScript 規範的 Prototype 繼承機制有些詭秘。 我們定義在建構者內的成員(寫在 new Function(){...} 中的變數與函數等),屬於建構者私有的 Prototype ,只有直接透過此建構者配置出來的個體可以用。 如果我們想要定義可以被衍生的建構者繼承的內容,我們要添加到基礎建構者的公開 Prototype 。

在此例中(完整源碼在文末),我們先定義了名叫 Class 的建構者,再從此衍生一個名叫 Data 的建構者。 接著我們想要在 Class 上定義 foreachaccssor可繼承方法,讓衍生的 Data 建構者可以使用,那我們就要把這兩個函數添加到 Class.prototype 此一公開的 Prototype 。 故而形成了先在第5行配置建構者,再在第18行與第32行添加方法的寫法。

如果對 JavaScript 的成員封裝機制有興趣,請繼續看我先前寫的 掌握 JavaScript 的「封裝」特性, part 1

中介編程技巧示範

接下來要示範兩種最常見的中介編程技巧,我們要加上 foreach 和預設存取器 accessor 功能。

第一個中介編程技巧 foreach

請看源碼第18到22行。如你所見,非常簡單的演算法。透過函數個體與匿名函數,我自定了一個 foreach 語法。 用匿名函數定義你的迭代器(iterator),傳給 foreach 即可。第83-85行是 foreach 的使用範例。 不過我沒有把這個 foreach 能力塞到最基底的 Object 類別中,所以 foreach 只作用於 Class 的衍生類別中。

這是個非常基本的技巧,但是用途很廣。 流行的 JavaScript 框架,如 Prototype, jQuery, Dojo, Yahoo UI 都實作了這項能力。 當然他們實作的 foreach 功能都比我這個示範用的強 :P

如果對 JavaScript 的匿名函數有興趣,請繼續看我先前寫的 The practice of anonymous recursion function in JavaScript

第二個中介編程技巧 accessor

JavaScript 不支援 PHP magic method 或類似的功能(參閱活用 PHP5 的 magic methods),也不支援 Ruby 的 accessor 或是 C# 3.0 的自動屬性完成語法,所以要用一些技巧實作。

調用 Function 的建構者時,會將最後一個參數轉為字串,視之為新函數的函數碼。
The last parameter provided to the Function constructor is converted to a string and treated as the FunctionBody.

ECMAScript Language Specification, page 37

利用這個規範,設計我的 accessor 語法。請看源碼第32到51行。 我自定的 accessor 語法是如下,接受一個 hash table 指示要套用預設存取器的屬性清單。 例如: accessor( {name1:1, name2:"abc"} )

至於 getter 和 setter 的寫法,則是按照動態語言常用的設計慣例,只用一個函數實現,這個函數只接受一個參數。 若調用屬性存取函數時無參數,則視為 getter 回傳屬性值。若給予一個參數,則視為 setter ,將屬性之值指派為參數值。 例如針對 content 屬性,就定義一個叫 content(value) 的存取函數(在本文中是中介編程自動定義)。 其運作內容如下:

function NewData() {
    this.props = {
        content: ''
    }

    this.content = function(value) {
        if (value == undefined)     //未傳遞參數,視為 getter
            return this.props.content;
        else                        //有參數,視為 setter
            this.props.content = value;
    }
}

var n = new NewData;
n.content("abc"); // setter
var s = n.content(); // getter
print(s);

本文透過中介編程自動產生了指定屬性的預設存取器函數,故而在源碼中看不到如上例般明顯示存取函數定義。 第108-110行則是示範利用中介編程產生的 setter 與 getter 存取 content 屬性。

data.js, 本文範例用源碼

/*
function Class() {
}
*/
Class = new Function;

/**
 * 上面先配置一個空的建構者,名叫 Class 。
 * 底下才是 Class 這個建構者所定義的可繼承方法。
 */

/**
 * 第一個中介編程技巧。
 * 透過函數個體與匿名函數,我自定了一個 foreach 語法。
 * 不過我沒有把這個 foreach 能力塞到最基底的 Object 類別中,所以 foreach 只作用於
 * Class 的衍生類別中。
 */
Class.prototype.foreach = function(f) {
    for (var p in this.props) {
        f(p, this.props[p]);
    }
}

/**
 * 第二個中介編程技巧。
 * 我自定的 accessor 語法是如下,接受一個 hash table 指示要套用預設存取器的屬性
 * 清單。
 * 例如: accessor( {name1:1, name2:'abc'} )
 *
 * @prarm props 要使用預設存取器的屬性列表(hash table).
 */
Class.prototype.accessor = function(props) {
    var p, sourceText;
    for (p in props) {
        sourceText = "  \
            if (v == undefined) \
                return this.props.{XXX}; \
            else    \
                this.props.{XXX} = v;   \
            ";

        this[p] = new Function("v", sourceText.replace(/{XXX}/g, p));
        // Script解譯器會自動將上面的源碼解釋為:
        //  this[p] = function(v) {
        //      if (v == undefined)
        //          return this.props.?
        //      else
        //          this.props.? = v;
        //  }
    }
}

function Data(args) {
    if (args == undefined)
        return;

    this.props = {
        id: true,
        title: true,
        content: true,
        timestamp: true
    }

    for (var p in args) {
        if (this.props[p])
            this.props[p] = args[p];
    }
}
Data.prototype = new Class;
//定義 Data 的原型是 Class 。亦即定義 Data 為 Class 的衍生類


var d = new Data({
    id: 1,
    title: 'rock',
    content: 'hello world',
    timestamp: (new Date()).toLocaleString()
});


// 得益於中介編程,在此使用自定的 foreach 語法:
print("Properties of Data");
d.foreach( function(name, value){
    print( name + " : " + value );
});


function DataDao(args) {
    if (args == undefined)
        return;

    Data.call(this, args);
    this.props.table = args.table;

    this.accessor(this.props);
}
DataDao.prototype = new Data;


var d2 = new DataDao({
    'id':  1,
    'title': 'rock',
    'content': 'hello world',
    'timestamp': (new Date()).toLocaleString(),
    'table': 'Data'
});

print("\n---- Accessor demo ----");
d2.content('hello javascript'); // setter
print( d2.content() ); //getter

print("\nProperties of DataDao");
d2.foreach( function(name, value) {
    print( name + " : " + value );
});

JavaScript 使用反射(reflection)的場合無所不在,在本文示範源碼中就一直在用。 但是不熟悉 JavaScript 的讀者可能會困惑「到底反射的程式碼寫在呢?」 有此困惑的讀者請先看 什麼是 reflection? , 看完 JavaScript 的反射語法後,再回來看本文就知道反射的程式碼寫在哪了。 在強型態的動態語言中,一個個體認識自己(自識)是再自然不過的事,所謂反射就像呼吸一樣自然,讓人感覺不到它的存在。

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

樂多舊回應
wancw.wang@gmail.com(WanCW) (#comment-20046335)
Mon, 09 Nov 2009 17:28:49 +0800
文章中的 foreach() 並未產生新的程式或是修改現有的程式,好像不太能算是 metaprogramming?