资讯专栏INFORMATION COLUMN

Javascript元编程(一)

mo0n1andin / 1452人阅读

摘要:中,便是两个可以用来进行元编程的特性。这也就是元编程的优点之一,程序可以根据传入参数对象的不同,动态地生成对应的程序,从而减少大量冗余的代码。

首发于知乎专栏:http://zhuanlan.zhihu.com/starkwang

这几天把一年多前买的《松本行弘的程序世界》重新看了看,很多当时不能理解的东西现在再去看真是茅塞顿开呀,看到元编程那一段真是把我震撼到了,后来发现 Javascript 里其实也是有一些支持元编程的特性的,今天就用一个 DEMO 示范一下吧。

什么元编程

“元编程”这个名字看起来高端大气上档次,它的含义也是相当高端:“写一段自动写程序的程序”,不要误会,我们做的可不是人工智能。

言简意赅地说,元编程就是将代码视作数据,直接用字符串 or AST or 其他任何形式去操纵代码,以此获得一些维护性、效率上的好处。

Javascript 中,evalnew Function()便是两个可以用来进行元编程的特性。

原始示例

现在我们有一堆用户的数据,具体字段有name,sex,age,address等等,通过类似 /get_name?id=123456 来拉取数据

那么我们很容易写出这样的代码:

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......
}

这段代码的问题在哪呢?

首先,用户数据有多少个字段,我们就要定义多少个 get_something 方法,更可怕的是这些方法里逻辑都是重复的,都是一个简单的 ajax。

进阶(一)

我们可以把拉取数据的逻辑封装到 __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}`)
    }
}
进阶(四)

下面我们让需求更加{{BANNED}}一点:

数据并非通过 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
参考

来来来,咱么元编程入个门

元编程之javascript

JavaScript 元编程之ES6 Proxy

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/87748.html

相关文章

  • Javascript 编程初探 [1]

    摘要:在这里讲到的很多也许只和程序对于工作机制的操作有关,但是作为初探也许也就足够了。一般情况下还有空字符串都会被判断成。面向特征编程面向特征编程的全称是。 引子 元编程会有如下的定义: 一种计算机程序的编写方式,它可以将其它程序(或者其本身)作为数据进行编写和操作,或者在编译时做一部分工作,在运行的时候做另外一部分工作。 在这里讲到的很多也许只和程序对于工作机制的操作有关,但是作为初...

    mcterry 评论0 收藏0
  • Javascript编程之Annotation

    摘要:事实上,实现元编程有多种方式,从语言本身来讲,可以分为两类增强型与新的语法实现,前者的代表是反射,后者的代表为。在第二部分,我们尝试在语言基础上增加原生的元编程能力并介绍了该思路的实现框架。 语言的自由度 自由度这个概念在不同领域有不同的定义,我们借鉴数学中构成一个空间的维数来表达其自由度的做法,在此指的是:解决同一个问题彼此不相关的设计方法学数量。 例如,解决一个比如商品打折的问题,...

    SegmentFault 评论0 收藏0
  • Javascript编程之Annotation

    摘要:事实上,实现元编程有多种方式,从语言本身来讲,可以分为两类增强型与新的语法实现,前者的代表是反射,后者的代表为。在第二部分,我们尝试在语言基础上增加原生的元编程能力并介绍了该思路的实现框架。 语言的自由度 自由度这个概念在不同领域有不同的定义,我们借鉴数学中构成一个空间的维数来表达其自由度的做法,在此指的是:解决同一个问题彼此不相关的设计方法学数量。 例如,解决一个比如商品打折的问题,...

    lifesimple 评论0 收藏0
  • javascript语言精髓与编程实践摘要

    摘要:逻辑运算一般语言中,逻辑运算与布尔元算是等义的,其运算元与目标类型都是布尔值。除此之外,还有以下的两条特性运算符会将运算元理解为布尔值,以进行布尔运算。运算过程是支持布尔短路的。 逻辑运算 一般语言中,逻辑运算与布尔元算是等义的,其运算元与目标类型都是布尔值。JavaScript当然支持这种纯布尔运算,不但如此,JavaScript还包括另外一种逻辑运算,它的表达式结果是不确定的。 ...

    shiina 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<