JavaScript的中介編程與反射能力示範
基於某些原因,我這幾天嘗試分別以 JavaScript, PHP, Ruby (排名按字母順序) 實現同一個簡單的功能,這個功能用了簡單的反射與中介編程技巧。 主要目的是看這些語言在動態型別、中介編程、迭代與反射語法方面的表現。 最後,我會用 Java 語言來實現這個需求,「展現Java語言的特點」。
這篇構想中的文章,愈寫愈長。我想了想,還是按程式語言拆成幾篇,先把要示範的源碼與說明發佈上來。 這是第一篇發佈的,內容是 JavaScript 的實作,示範了兩個基本的中介編程技巧: foreach 和 accessor。
建構者(Constructor)
所有建構者都是個體,但並非每個個體都是建構者。
ECMAScrpit Language Specification, page 3
All constructors are objects, but not all objects are constructors.
所以 Abc = {};
就只是配置一個單純的個體,而不會產生一個建構者。
那麼建構者從何而來?
建構者是一個具有建立與賦予初值給個體之能力的函數實體。
ECMAScript Language Specification, page 4
A constructor is a Function object that creates and initialises objects.
和 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 上定義 foreach 與 accssor 的可繼承方法,讓衍生的 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 的建構者時,會將最後一個參數轉為字串,視之為新函數的函數碼。
ECMAScript Language Specification, page 37
The last parameter provided to the Function constructor is converted to a string and treated as the FunctionBody.
利用這個規範,設計我的 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 的反射語法後,再回來看本文就知道反射的程式碼寫在哪了。 在強型態的動態語言中,一個個體認識自己(自識)是再自然不過的事,所謂反射就像呼吸一樣自然,讓人感覺不到它的存在。
相關文章
- 再探 JavaScript的中介編程 foreach
- PHP的中介編程與反射能力示範
- JavaScript的類別定義擴充能力
- Ruby的中介編程與反射能力示範
- 從中介編程與反射能力來談 Java 語言
- 產生新類別的類別
樂多舊回應