首发于知乎专栏: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