不由分說,JavaScript本身的動態性,使之在進行meta程式設計(meta-programming)時,極具彈性,因而在ES6之前,有著各式風格的程式庫與自動補全(polyfill),進而形成了JavaScript生態圈的多樣性(與分岐)。

等到後來出現的ES6,除了基於這些程式庫與自動補全制定標準,在meta程式設計上,也提供了Symbol、Proxy與Reflect API作為標準工具。

在ES6前的meta程式設計

正如之前專欄〈動態擴展語言元素的程式設計〉中談過的,meta程式設計本身,並沒有嚴謹的定義,大致上,就是代表著執行時期的程式碼產生與運行,以及物件行為修改、物件行為檢驗等設計的概念。

從JavaScript誕生以來,就執行時期的程式碼產生與運行這方面而言,就存在著eval函式;就修改物件行為上,JavaScript支援物件個體化,而基於原型的物件導向特性,執行時期要修改建構式(類別)定義,也極為容易;至於在檢驗物件行為上,也有in、instanceof、typeof等運算子,或者是for in之類的語法可供使用。

ECMAScript 5時,Object上出現了一組靜態方法,其中的defineProperty、defineProperties方法,可為物件定義出更細緻的特性描述,像是可否修改(writable)、列舉(enumerable)與組態(configurable);進一步地,透過在存取描述器(Accessor descriptor)指定get、set方法,可對指定的特性進行更為深入的存取控制。

物件的特性並非都可列舉,而不可列舉的特性,無法透過for in來迭代,雖然Object.keys靜態方法可以傳回特性名稱組成的陣列,不過,也限於可列舉的特性,陣列中的特性順序與for in相同,以符合既有的瀏覽器實作,然而,這順序不在規範之內,也就是說,在不同的瀏覽器實作下,for in、Object.keys的順序可能不同。

若想包含不可列舉的特性,我們須透過Object.getOwnPropertyNames靜態方法,傳回的特性名稱陣列中,可列舉的特性順序與for in、Object.keys的順序相同,而不可列舉的特性彼此間的順序,以及它們與可列舉特性間的順序,則沒有定義。

就結論來說,ES6之前的meta程式設計,主要依賴在物件個體化、JavaScrpt本身的語法(像是in運算子、for in語法等)以及Object上的靜態方法,說是為了meta程式設計而存在,其實並不十分正確,而是剛好這些特性可用於meta程式設計。

符號(Symbol)

先前談到列舉特性時的順序保證,到了ES6中,已經有了規範。例如,ES6中的Reflect.ownKeys靜態方法,採用的是(在規範中代表瀏覽器實作時應遵守的演算或行為),數值索引會以遞增方式列舉,接著是特性被建立的順序,然後才是符號被建立的順序。

在另一篇專欄文章〈那些語言中的符號型態〉裡,我談過不同程式語言中的符號,裡面也提到ES6支援符號的概念,而且,所使用的符號,是獨一無二且不可列舉的,也不會受到Object.getOwnPropertyNames列舉,此時,我們必須透過Object.getOwnPropertySymbols,才有辦法,這也表示,可以透過符號來修改既有的建構式或物件,又不用擔心發生名稱衝突問題。

因此,在meta程式設計上,可以使用符號特性來儲存、作為一種meta特性(meta property),或者通俗點的說法,就是定義物件間特定的掛勾,像ES6中提供了Symbol.iterator、Symbol.hasInstance等已知符號(Well-Known Symbol),藉由定義這些已知符號,可以為物件增添特定的行為。

Proxy與Reflect

透過Object.defineProperty可以定義存取描述器,而在ES6中,物件實字也支援get、set方法,可直接為指定的特性名稱,來定義設值方法(setter)與取值方法(getter)。某些程度上,設值與取值方法是一種攔截器的概念,開發者可以攔截對特性的操作,用以修正、改變物件的行為。

如果想要攔截的對象,不是特定名稱,而是全部的特性呢?

在ES6可以透過Proxy來達到,在建立Proxy實例時,必須指定目標物件,並註冊一個有特定協定的處理器,例如,處理器若定義有get(target,key,proxy),那麼,透過Proxy實例指定取得某特性時,就會執行處理器的get方法,傳入目標物件、指定的特性名稱,以及被操作的Proxy實例。

然而,get的參數會讓人聯想到反射(Reflection)機制,實際上,處理器的協定不只有get,還有set、apply、getOwnPropertyDescriptor、defineProperty等,這又會讓人聯想到Object上定義的那些靜態方法,不過,支持著Proxy的API並不是Object API,而是ES6新增的Reflect API。

實際上,在ECMAScript中,有著代表、、之類的規範。在實現JavaScript引擎時,這些規範裡面,部分被實現為引擎的內部方法,而有些API會使用到這些內部方法。

例如,在ES6中,Object.keys會以取得鍵,然後濾掉不可列舉的特性,並重新安排順序以符合特定瀏覽器既有實現,有些內部方法,則在透過運算子或者是語法時運用,像是delete運算子,會使用到規範的演算。

Reflect API提供了統一的途徑來取用這些內部方法,像是Reflect上的get、ownKeys、deleteProperty等靜態方法,可用來取用[Get]]、、等內部方法,如此就不用組合Object上的某些API來達到目的,也讓內部方法的取用,都變成了函式的行為。

一些明顯不屬於物件的方法,像是apply,被放到了Reflect,這也就是為什麼get、ownKeys等不像之前那樣,被放到Object。而有些Object上的方法,在Relect中,雖也有個對應的版本,然而行為被修正,例如:Reflect的getOwnPropertyDescriptor,在特性不存在時,會拋出TypeError,而Object的版本則是傳回undefined。

Reflect很大程度上,是為了Proxy而存在。Proxy上的meta特性,在Reflect上都有一對一的版本,而Reflect可以處理更低層次的資訊,像是透過Reflect.get的第三個receiver參數,可以用來控制取值方法的this參考對象,〈Benefits of ES6 Reflect API〉(https://goo.gl/9v9STM)中,就有個例子,必須同時結合Proxy與Reflect,才能涵蓋整個目標物件的特性存取。

meta程式設計的利器

過去想要在JavaScript中,從事meta程式設計,比較沒有明確的方式,要嘛就是在語言中尋找適當的運算子或語法,要不就在收集散落且與特定實現相關的API,像是Object上的靜態方法。

如果使用ES6,打算實現meta程式設計的概念,現在,可以直接從符號、Proxym,或者更低層次的Reflect來下手了,不僅方便且有著一致的行為,如果有興趣看更多技術細節的話,可以看看〈Metaprogramming in JavaScript〉(https://goo.gl/adGCC1),或者是〈Metaprogramming in ES6〉(https://goo.gl/zzhQTc)系列的文件(有Part1到Part3)。

在《你不知道的JS:ES6與未來發展》的〈Meta programming〉這一章中,也有一些應用Proxy的精采範例,談到了proxy first、proxy last的概念,足以開啟我們在JavaScript從事meta程式設計時的想像力。

專欄作者

熱門新聞

Advertisement