首发于知乎专栏:http://zhuanlan.zhihu.com/starkwang
这几天把一年多前买的《松本行弘的顺序天下》从新看了看,许多当时不能明白的东西如今再去看真是恍然大悟呀,看到元编程那一段真是把我震动到了,厥后发明 Javascript 里实在也是有一些支撑元编程的特征的,本日就用一个 DEMO 树模一下吧。
什么元编程
“元编程”这个名字看起来高端大气上档次,它的寄义也是相称高端:“写一段自动写顺序的顺序”,不要误解,我们做的可不是人工智能。
一针见血地说,元编程就是将代码视作数据,直接用字符串 or AST or 其他任何情势去支配代码,以此取得一些保护性、效力上的长处。
Javascript 中,eval
、new 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}`)
}
}
进阶(四)
下面我们让需求越发变态一点:
数据并不是经由过程 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