Javascript元編程(一)

首發於知乎專欄:http://zhuanlan.zhihu.com/starkwangjavascript

這幾天把一年多前買的《松本行弘的程序世界》從新看了看,不少當時不能理解的東西如今再去看真是茅塞頓開呀,看到元編程那一段真是把我震撼到了,後來發現 Javascript 裏其實也是有一些支持元編程的特性的,今天就用一個 DEMO 示範一下吧。html

什麼元編程

「元編程」這個名字看起來高端大氣上檔次,它的含義也是至關高端:「寫一段自動寫程序的程序」,不要誤會,咱們作的可不是人工智能。java

言簡意賅地說,元編程就是將代碼視做數據,直接用字符串 or AST or 其餘任何形式去操縱代碼,以此得到一些維護性、效率上的好處。ajax

Javascript 中,evalnew 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}`)
    }
}

進階(四)

下面咱們讓需求更加變態一點:

  1. 數據並不是經過 ajax 直接拉取,而是經過一個別人封裝好的 UserDataBase 裏的方法來拉取;

  2. 數據的字段並不是只有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

參考

來來來,咱麼元編程入個門

元編程之javascript

JavaScript 元編程之ES6 Proxy

相關文章
相關標籤/搜索