前言
js中一直以来都有元编程的概念,比如ES5中强大的Object.defineProperty,ES6中Proxy + Reflect更是将这一能力发扬光大。
首先我们首先需要了解什么是元编程?在编程中,按层次可划分为以下级别:
- 应用程序级别,代码用来处理用户输入
- 在元级别,代码用来处理基础的语言级别的功能,甚至可以改变一些语法的默认行为。
这也是元编程在日常开发中用的很少的原因,因为它的应用场景更多的是在一些通用的库与框架中,比如
vue中,它的核心功能是:
将对象的属性修改与视图更新映射起来,那么就需要监听到针对对象属性的所有操作,并进行相应的拦截,中间增加一层vue视图更新的逻辑。
到这里,假如你是vue的设计者,不妨思考下,应该暴露什么样级别的api给开发者呢? - 第一种,提供一套专门的api调用,比如想触发get依赖收集需要开发者手动调
vue.$get方法,发布依赖更新主动调vue.$set方法),那么template里取值的地方都需要调专用的$get,极其繁琐。 - 第二种,语言级别的实现。也就是之前你咋写js还是咋写,依然是
obj.xxxobj.xxx = val这种最基础的对象属性取值、赋值语法,剩下的一切都交给框架,不破坏开发者原先的心智模型。 毫无疑问,一定是第二种语言级别的实现是最为简洁的,框架在使用层面推广起来也最为友好,所以元编程思想我们并不陌生,熟悉的vue等依赖mutable对象的mvvm框架就是其中一种很强大的应用。
在js中,属于元编程范畴的有 - Object上的静态方法都具有元编程的功能,比如
definedProperty可以重新定义对象属性的默认操作行为。 - 通过
Symbol的Symbol.toStringTagSymbol.toPrimitive等静态属性可以读取并改写这些 语言内部 的方法。 eval(code)、new Function(...args,code)这些可以动态编译执行js语句的方法- 原生提供的代理模式实现
Proxy本文重点会介绍Proxy和与之相对的Reflect。
正文
Proxy
代理,是一种非常经典的设计模式,ES6中提供的Proxy api,就是对这一模式在代理对象操作方面的实现。它的工作方式如下:
const xiaoming = {
age: 30,
};
const handler = {
get() {
return 18;
},
};
const proxyedTarget = new Proxy(xiaoming, handler);
console.log(proxyedTarget.age); // 18
在这个例子中,小明依靠Proxy实现了自己永远18岁的美好愿望。这是因为obj.xxx执行触发了读取属性值的【Get】操作,会默认触发代理对象的get捕获,在方法中可以将返回值进行修改。
捕获操作与对应的js触发语法
- 【
get】obj.xxxobj[xxx]Reflect.get对象取值语法 - 【
set】obj.xxx = valReflect.set对象属性的设置语法 - 【
has】key in obj判断属性是否存在的操作符 - 【
apply】函数类型的对象调用fn() - 【
construct】函数对象的new fn()调用 - 【
defineProperty】对应Object.defineProperty调用 - 【
deleteProperty】delete obj[foo]delete obj.foo或者Reflect.deleteProperty - 【
getOwnPropertyDescriptor】对应Object.defineProperty调用 - 【
getPrototypeOf】对应Object.getPrototypeOf调用 - 【
setPrototypeOf】是Object.setPrototypeOf方法的捕捉器 - 【
ownKeys】是Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols方法的捕捉器。 - 【
preventExtensions】是Object.preventExtensions方法的捕捉器 - 【
isExtensible】 是Object.isExtensible方法的捕捉器。
Reflect
为Proxy而生
js中Reflect与其他语言(如java、python)中的Reflect的意义有所不同,因为它在js中,是为了更方便的使用Proxy才出现的,因为它的方法与Proxy完全一一对应,下边这个例子就是最好的佐证。
比如让你设计一个功能,对一个对象的所有操作都log下来。
function log(target){
const handler = {};
Reflect.ownKeys(Reflect).forEach((op)=>{
handler[op] = (...args)=>{
console.log(op,args)
return Reflect[op](...args)
}
})
return new Proxy(obj,handler)
}
const obj = {
age:30
}
const proxyedObj = log(obj)
proxyedObj.age = 18
proxyedObj.age
执行结果
可以想象,如果你手动对上边提到的Proxy能代理到的10种的操作都进行分别处理,那代码得写多长。。。Reflect让这一切都变得非常简单,针对Proxy的每一个能被代理的方法,都有对应的操作进行处理,而且参数与方法名完全一致,就像一面镜子那样会反射对一个代理对象的所有操作,这也是它的最大价值所在。
除此之外,Reflect还带来了其他的一些好处:
替代Object
这其实也是ES官方一个无心插柳柳成荫的创意,上边讲到,Proxy能代理的操作类型,除了一些来源于obj.xxx in的语法操作外,其他都基本来自Object这个方法的静态方法调用,Reflect为了实现与Proxy的操作反射,必然也要在自身上实现一套Object的静态方法,而且在这些方法迁移过程中,修正了一些之前设计不好的地方,比如object.defineProperty(obj, name, desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false,并且,增加了receiver参数,这就导致Reflect相比Object设计上更安全,功能上更强大,直接可以取代了Object上的静态方法,ES官方表示,以后新增的对象操作方法,都会直接在Reflect身上实现,Object身上的就不会更新了,就让它最大程度的保持一个对象的构造函数的功能就好了。
运行时特性
可以想象,Proxy代理的操作,都是在js运行时触发的,那么Reflect的反射操作,也当然都是在运行时调用,利用这个特性,可以绕过一些静态强类型的类型限制,比如在TypeScript中。
遍历对象场景时,常常会遇到这个错误
原因其实很明显:遍历过程中的 item 属性变量只约束为string类型,太宽泛了,应该只限定为对象存在的属性字符串,访问未知的属性会报错
这个固然可以用TS的类型断言方式来解决
在这里,还可以利用Reflect的运行时特性,去躲避TS的静态类型校验。
结束
以上就是关于Proxy Reflect的全部介绍及自己一些使用经验。