高级语言其实一直在享受元编程带来的抽象与高校,我们常见的各种编译器就算是一个元编程工具,而且对于大多数程序员来说现在几乎不会再去碰汇编或者机器语言,还有类似于 Python 中装饰器,JS 中的eval
,new Function()
这类方式生成代码.甚至低级语言 Rust 中都有宏macro方式来让程序员可以更加细致化的定制代码的方方面面.
下面就介绍 JS 相关的元编程,顺便整理一下这类语法的使用,如果能应用到实际工作中去,那么带来的高效是实实在在的,还可以早点下班.
准备工作
首先要了解到 ES6 中新增的Symbol
,Proxy
,Reflect
三个 API.
Symbol
提供唯一值作为永远不会冲突的对象属性名.Proxy
就像是一个代理器,提供对象的各种代理.Reflect
可以配合 proxy 使用,提供一系列的静态方法.
Reflect
还是可以介绍一下的,这个也是一个全局 API,类似于Math
,JSON
这类,它主要的作用是提供原本在各个对象原型中的一些静态方法,此博文最下面的相关知识点
有它的一些对应列表.
Symbol
就不做介绍了,Google 很多相关的.
在介绍Proxy
之前我们要先介绍一下 JavaScript 中另一个神奇的东西----Property Descriptor
,一个叫属性描述符的东西.如果你不认识,可以看看我这篇博文深入理解 JavaScript 对象.
开始
Proxy
接受两个参数Proxy(target, handler)
,其中target
可以是Object
,Function
,Class
类型,而handler
是一个对象,其中可以包含以下方法:
- getPrototypeOf()
- setPrototypeOf()
- isExtensible()
- preventExtensions()
- getOwnPropertyDescriptor()
- defineProperty()
- has()
- get()
- set()
- deleteProperty()
- ownKeys()
- apply()
- construct()
先看一下一个小例子:
class Site {
constructor(name, tld) {
this.name = name;
this.tld = tld;
}
get url() {
return `${this.name}.${this.tld}`;
}
}
const site = new Site('obeta', 'net');
const siteProxy = new Proxy(site, {
get(target, prop, receiver) {
if (prop === 'url') return Reflect.get(target, prop);
if (prop === 'fullUrl') {
return `https://${Reflect.get(target, 'url')}`;
}
},
});
console.log(siteProxy.url); // obeta.me
console.log(siteProxy.fullUrl); // https://obeta.me
console.log(siteProxy.name); // undefined
上面代码中我们代理了一个类的实例site
为siteProxy
,通过使用siteProxy
我们可以对访问做验证,也可以定制一个特殊的属性如fullUrl
.
也可以使用apply
对函数的调用次数做统计:
function sum(a, b) {
return a + b;
}
const useLogger = new Proxy(sum, {
apply(target, thisArg, argsList) {
// thisArg 调用时的上下文对象
console.log('call in', argsList); // 统计
return Reflect.apply(target, thisArg, argsList);
// return target(...argsList);
},
});
console.log(useLogger(1, 2));
Proxy 的使用场景非常的多,其中一个用处是在 API 请求方面,我们可以制作一个空对象,对空对象使用get
代理:
const axios = require('axios');
const request = axios.create({
baseURL: 'https://obeta.me/api',
timeout: 100000,
});
module.exports = new Proxy(
{},
{
get(target, name) {
return Object.assign(
{},
['get', 'post', 'delete', 'put', 'patch', 'head', 'option'].reduce(
(obj, method) => {
obj[method] = (...args) => {
const path = name
.replace(/([a-z])([A-Z])/g, '$1/$2')
.replace(/\$/g, '/$/')
.toLowerCase();
const requestConfig = {
url: path.replace(/\$/g, () => args.shift()),
method,
};
if (method === 'get' || method === 'delete') {
requestConfig.params = args.shift() || {};
} else {
requestConfig.data = args.shift() || {};
}
return request(requestConfig);
};
return obj;
},
{}
)
);
},
}
);
上面核心点是使用了replace
方法,如果你对这个方法不是很了解,可以查看我这篇博文JavaScript 中的 replace 你真的知道怎么用吗?
使用方式很简单:
const api = require('./api');
api.user.get(); // get /api/user
api.user.post({
name: 'obeta',
age: 25,
}); // post /api/user
api.userInfo.get(); // get /api/user/info
api.user$Info.post(1233, {
name: 'obeta',
age: 24,
}); // post /api/user/1233/info
api.user$Info$.get(1233, 'detail', {
name: 1,
}); // get /api/user/1233/info/detail?name=1
可撤销的 Proxy
是的,JS 也提供了这方面的需求,比如我们工作中后端可能只提供了部分 API,另外部分 API 还未完成(经常的事),因此我们可以使用可撤销的 Proxy.
// ...
// Create a revocable proxy
let { proxy, revoke } = Proxy.revocable(payload, {
get(...args) {
console.log('Proxy');
return Reflect.get(...args);
},
});
// proxy 是我们需要使用的
// 调用revoke可以取消代理, proxy不可再次使用
revoke();
相关知识点
Reflect
包含的方法及对应相同功能的方法,方法的返回值为boolean
代表其是否执行成功:
apply
construct
defineProperty
deleteProperty
get
getOwnPropertyDescriptor
getPrototypeOf
has
isExtensible
ownKeys
preventExtensions
set
setPrototypeOf
具体可以去MDN查看.