首發於知乎專欄:http://zhuanlan.zhihu.com/starkwangjavascript
這幾天把一年多前買的《松本行弘的程序世界》從新看了看,不少當時不能理解的東西如今再去看真是茅塞頓開呀,看到元編程那一段真是把我震撼到了,後來發現 Javascript 裏其實也是有一些支持元編程的特性的,今天就用一個 DEMO 示範一下吧。html
「元編程」這個名字看起來高端大氣上檔次,它的含義也是至關高端:「寫一段自動寫程序的程序」,不要誤會,咱們作的可不是人工智能。java
言簡意賅地說,元編程就是將代碼視做數據,直接用字符串 or AST or 其餘任何形式去操縱代碼,以此得到一些維護性、效率上的好處。ajax
Javascript 中,eval
、new Function()
即是兩個能夠用來進行元編程的特性。編程
如今咱們有一堆用戶的數據,具體字段有name
,sex
,age
,address
等等,經過相似 /get_name?id=123456
來拉取數據babel
那麼咱們很容易寫出這樣的代碼:函數
class User { constructor(userID) { this.id = userID; } get_name() { return $.ajax(`/get_name?id=${this.id}`); } get_sex() { return $.ajax(`/get_sex?id=${this.id}`); } //下面是get_age、get_address...... }
這段代碼的問題在哪呢?fetch
首先,用戶數據有多少個字段,咱們就要定義多少個 get_something
方法,更可怕的是這些方法裏邏輯都是重複的,都是一個簡單的 ajax。this
咱們能夠把拉取數據的邏輯封裝到 __fetchData
裏:人工智能
class User { constructor(userID) { this.id = userID; } __fetchData(key) { //這是一個private方法,直接調用相似__fetchData("age")是不被容許的 return $.ajax(`/get_${key}?id=${this.id}`) } get_name() { return this.__fetchData('name'); } get_sex() { return this.__fetchData("sex"); } //下面是get_age、get_address...... }
而後,冗餘的問題能夠經過registerProperties
來解決:
class User { constructor(userID) { this.id = userID; this.registerProperties(["name", "age", "sex", "address"]); } registerProperties(keyArray) { keyArray.forEach(key => { this[`get_${key}`] = () => this.__fetchData(key); }) } __fetchData(key) { //這是一個private方法,直接調用相似__fetchData("age")是不被容許的 return $.ajax(`/get_${key}?id=${this.id}`) } }
到目前爲止咱們都沒有涉及到任何元編程的概念,下面咱們加上更高的需求:
在拉去數據以後,咱們要對部分數據進行必定的處理,好比對 name
咱們要去掉首尾的空格,對 age
咱們要加上一個 歲
字。具體的處理方法定義在 __handle_something
裏面。
這裏咱們即可以經過 new Function()
來動態生成函數,元編程開始顯現威力:
class User { constructor(userID) { this.id = userID; this.registerProperties(["name", "age", "sex", "address"]); } registerProperties(keyArray) { keyArray.forEach(key => { //注意這裏的fnBody內部依然採用ES5的寫法,由於babel目前不會編譯函數字符串。 var fnBody = `return this.__fetchData("/get_${key}?id=${this.id}") .then(function(data){ return this.__handle_${key}?_this.handle_${key}(data):data; })`; this[`get_${key}`] = new Function(fnBody); }) } __handle_name(name) { //do somthing with name... return name; } __handle_age(age) { //do somthing with age... return age; } __fetchData(key) { //這是一個private方法,直接調用相似__fetchData("age")是不被容許的 return $.ajax(`/get_${key}?id=${this.id}`) } }
下面咱們讓需求更加變態一點:
數據並不是經過 ajax 直接拉取,而是經過一個別人封裝好的 UserDataBase
裏的方法來拉取;
數據的字段並不是只有name
,sex
,age
,address
四個,而是要根據 UserDataBase
裏給你的方法決定。給你1000個get不一樣字段的方法,User類裏也要有對應的1000個方法。
class UserDataBase { constructor() {} get_name(id) {} get_age(id) {} get_address(id) {} get_sex(id) {} get_anything_else1(id) {} get_anything_else2(id) {} get_anything_else3(id) {} get_anything_else4(id) {} //...... }
這裏咱們就須要用到 JS 的反射機制來讀取全部拉取字段的方法,而後經過元編程的方式來動態生成對應的方法。
class User { constructor(userID, dataBase) { this.id = userID; this.__dataBase = dataBase; for (var method in dataBase) { //對每個方法 this.registerMethod(method); } } registerMethod(methodName) { //這裏除去了前置的"get_" var propertyName = methodName.slice(4); //注意這裏拉取數據的方法改成使用dataBase var fnBody = `return this.__dataBase.${methodName}() .then(function(data){ return this.__handle_${propertyName}?_this.handle_${propertyName}(data):data; })`; this[`get_${propertyName}`] = new Function(fnBody); } __handle_name(name) { //do somthing with name... return name; } __handle_age(age) { //do somthing with age... return age; } } var userDataBase = new UserDataBase(); var user = new User("123", userDataBase);
這樣即便用戶數據有一萬種不一樣的屬性字段,只要保證 UserDataBase
中良好地定義了對應的拉取方法,咱們的 User
就能自動生成對應的方法。
這也就是元編程的優勢之一,程序能夠根據傳入參數/對象的不一樣,動態地生成對應的程序,從而減小大量冗餘的代碼。
如今程序裏還有點小瑕疵:
//用戶數據中不存在www字段,若這樣執行會報錯: user.get_www(); //user.get_www is not a function
如今咱們要保證像上面那樣執行任意的 user.get_xxxx()
,程序不會報錯,而是返回 false
:
//用戶數據中不存在www字段: user.get_www(); // => false
Javascript 裏缺乏了 Ruby 中 method_missing
這樣黑科技的內核方法,可是咱們能夠經過 ES6 的 Proxy 特性來模擬:
function createUser(id, userDataBase) { return new Proxy(new User(id, userDataBase), { get: (target, property) => (typeof(target[property]) === "function" ? target[property] : () => false) }) } var userDataBase = new UserDataBase(); var user = createUser("123", userDataBase); user.get_name() => // fetch name data user.get_wwwwww() // => false
其實這裏的 DEMO 只是元編程的一個小應用,下一篇文章裏咱們會經過元編程實現一個簡單的表單驗證 DSL :
//相似 form.name["is not empty"]["length is between",1,20] // => true or false