基於某些原因,我這幾天嘗試分別以 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 的建構者。
2.配置一個新函數,指派給變量 F。
這兩種寫法等義。
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 的建構者時,會將最後一個參數轉為字串,視之為新函數的函數碼。
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) 的存取函數(在本文中是中介編程自動定義)。
其運作內容如下:
本文透過中介編程自動產生了指定屬性的預設存取器函數,故而在源碼中看不到如上例般明顯示存取函數定義。
第108-110行則是示範利用中介編程產生的 setter 與 getter 存取 content 屬性。
data.js, 本文範例用源碼
JavaScript 使用反射(reflection)的場合無所不在,在本文示範源碼中就一直在用。
但是不熟悉 JavaScript 的讀者可能會困惑「到底反射的程式碼寫在呢?」
有此困惑的讀者請先看 什麼是 reflection? ,
看完 JavaScript 的反射語法後,再回來看本文就知道反射的程式碼寫在哪了。
在強型態的動態語言中,一個個體認識自己(自識)是再自然不過的事,所謂反射就像呼吸一樣自然,讓人感覺不到它的存在。
網友 WanCW 在 JavaScript的中介編程與反射能力示範 一文中回應 文章中的 foreach() 並未產生新的程式或是修改現有的程式,好像不太能算是 metaprogramming?
並非如此,其實 foreach 在中介編程(metaprogramming)的領域是經典樣式。只是我上文的例子太精簡,以至於看不出它的威力。嗯,如果不來個複雜點的程式碼,確實不容易看出 foreach 到底可以幫我們省下多少程式碼。我就來個複雜點的示範吧。
PHP的中介編程與反射能力示範【石頭閒語】
at 2009年11月11日 14:47
JavaScript的類別定義擴充能力【石頭閒語】
at 2009年11月12日 23:19
Ruby的中介編程與反射能力示範【石頭閒語】
at 2009年11月15日 21:23
我不算聰明的開發者,但十幾年基本功練下來,功底還算紥實。OOP, ORM, Design pattern 這些概念都懂。Web 架構更是熟悉。儘管如此,當我試圖使用 Spring, Hibernate 等框架時,我完全無法理解為什麼 Java 語言可以把一件簡單的事搞成這麼複雜。這件事促成我寫出《不同程式語言的中介編程與反射能力系列文章》
從中介編程與反射能力來談 Java 語言【石頭閒語】
at 2009年11月16日 02:14
文章中的 foreach() 並未產生新的程式或是修改現有的程式,好像不太能算是 metaprogramming?