Obeta

JavaScript中的元编程

元编程就像是我们写一个程序A,让它来生成程序B来执行,元编程让我们拥有了定制语言底层机制的能力.

高级语言其实一直在享受元编程带来的抽象与高校,我们常见的各种编译器就算是一个元编程工具,而且对于大多数程序员来说现在几乎不会再去碰汇编或者机器语言,还有类似于 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

上面代码中我们代理了一个类的实例sitesiteProxy,通过使用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查看.

引用

  1. wikipedia
  2. symbol
  3. reflect
  4. proxy
  5. How to use JavaScript Proxies for Fun and Profit
  6. Meta_programming

个人随笔记录,内容不保证完全正确,若需要转载,请注明作者和出处.