Proxy
Proxy 介绍
什么是 Proxy?
Proxy 用于创建一个对象的代理,从而实现基本操作的拦截并自定义行为(如属性查找、赋值、枚举、函数调用等)。
Proxy 可以理解为在目标对象之前架设一层“拦截”,外界对该对象的访问都必须先通过这层拦截,因此提供了一种机制可以对外界的访问进行过滤和改写等操作。
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程(meta programming),即对编程语言进行编程。
Proxy 的语法
let proxy = new Proxy(target, handler)target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生对象、数组、函数、甚至另一个代理)。注意:target 必须是一个对象,不能是原始类型的值。handler:一个对象,用来定制拦截行为,其属性全部为函数类型。这些函数类型的属性被称为捕获器、拦截器或陷阱函数(trap),其作用就是基本操作的拦截并自定义行为(如属性查找、赋值等)。注意:这里的拦截其实是对代理对象(proxy)的基本操作的拦截,而不是对被代理的对象(target)的拦截
handler 对象总共有 13 个捕获器(trap):
get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。
如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, ctx, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(ctx, ...args)、proxy.apply(...)。construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
注意:
上面的每个捕获器都是可选的。如果没有设置某个捕获器,那么对应的操作会直接落在目标对象上。
Proxy 只能对对象的基本操作进行代理,无法代理复合操作
上述代码中,并没有拦截到 obj.fn() 函数调用操作,而却是只是输出了“我被读取了 fn 属性”。究其原因,我们可以再次从 Proxy 的定义里的关键词“基本操作”找到答案 。那么何为基本操作呢?在上述代码中就表明了对象属性的读取 p.foo 就是基本操作,与之对应的就是非基本操作,我们可以称之为复合操作。而 obj.fn() 就是一个典型的复合操作,它是由两个基本操作组成的分别是读取操作 obj.fn , 和函数调用操作(取到 obj.fn 的值再进行调用),而我们代理的对象是 obj,并不是 obj.fn。因此,我们只能拦截到 fn 属性的读取操作。这也说明了 Proxy 只能对对象的基本操作进行代理,这点尤为重要。
有些捕获器的参数中有
receiver,它指的是原始的操作所在的那个对象。通常情况下是指代理对象本身,即 Proxy 实例。
例 1:
上面代码中,proxy 对象的 getReceiver 属性会被 get()拦截,得到的返回值就是 proxy 对象。
例 2:
上面代码中,obj 对象本身没有 getReceiver 属性,所以读取 obj.getReceiver 的时候,会去 obj 的原型 proxy 对象找。这时,receiver 就指向 obj,代表原始的读操作所在的那个对象。
例 3:
上面代码中,设置 myObj.foo 属性的值时,myObj 并没有 foo 属性,因此引擎会到 myObj 的原型链去找 foo 属性。myObj 的原型对象 proxy 是一个 Proxy 实例,设置它的 foo 属性会触发 set 方法。这时,第四个参数 receiver 就指向原始赋值行为所在的对象 myObj。
如果目标对象自身的某个属性不可写(writable)且不可配置(configurable),那么 get 和 set 捕获器无法修改该属性,严格模式下会报错。
set 捕获器应当返回一个布尔值。严格模式下,set 代理如果没有返回 true,就会报错。
Proxy 的应用
数组负索引
内部属性的保护
运算符重载
解决层级很深的对象取值判断不严谨的问题
表单验证
参考:探索两种优雅的表单验证——策略设计模式和 ES6 的 Proxy 代理模式
Reflect
Reflect 设计的目的
(1) 将 Object 对象的一些明显属于语言内部的方法(比如 Object.defineProperty),放到 Reflect 对象上。现阶段,某些方法同时在 Object 和 Reflect 对象上部署,未来的新方法将只部署在 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。
(2) 修改某些 Object 方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回 false。
(3) 让 Object 操作都变成函数行为。某些 Object 操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
(4)Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。这就让 Proxy 对象可以方便地调用对应的 Reflect 方法,完成默认行为,作为修改行为的基础。也就是说,不管 Proxy 怎么修改默认行为,你总可以在 Reflect 上获取默认行为。
Reflect 的静态方法
Reflect 对象一共有 13 个静态方法。
Reflect.apply(target, thisArg, args):等同于Function.prototype.apply.call(target, thisArg, args),用于绑定函数的this对象,并执行函数。Reflect.construct(target, args):等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。Reflect.get(target, name, receiver):获取对象身上某个属性的值,类似于target[name]。Reflect.set(target, name, value, receiver):设置对象身上某个属性的值,类似于target[name] = value。Reflect.defineProperty(target, name, desc):类似于Object.defineProperty,用于在对象上定义一个新属性或修改一个已经存在的属性,并返回一个布尔值。Reflect.deleteProperty(target, name):类似于delete obj[name],用于删除对象的属性,返回一个布尔值。Reflect.has(target, name):类似于name in obj,用于检查对象是否具有某个属性,返回一个布尔值。Reflect.ownKeys(target):类似于Object.getOwnPropertyNames和Object.getOwnPropertySymbols的合集,用于获取对象的所有属性名,返回一个数组。Reflect.getOwnPropertyDescriptor(target, name):类似于Object.getOwnPropertyDescriptor,用于获取对象指定属性的描述对象。Reflect.getPrototypeOf(target):类似于Object.getPrototypeOf,用于获取对象的原型对象。Reflect.setPrototypeOf(target, prototype):类似于Object.setPrototypeOf,用于设置对象的原型对象。Reflect.preventExtensions(target):类似于Object.preventExtensions,用于让一个对象变为不可扩展,返回一个布尔值。Reflect.isExtensible(target):类似于Object.isExtensible,用于判断一个对象是否可扩展,返回一个布尔值。
Reflect 在 Proxy 中的应用
定义了一个 foo 属性和 bar 属性,其中 bar 属性是一个访问器属性,通过 get 函数 return this.foo 获取得到的,因此按理来说我们在读取 bar 属性时候会触发读取 foo 属性,也同样会被 get 的 trap 所拦截到,但实际代码运行结果并没有拦截到 foo 属性。
这是为什么呢?答案的关键在于 bar 访问器里的 this 指向。梳理下代码运行过程:p.bar 实际上会被 handler 的 get 捕获,返回 target['bar'],而这里的 target 实际上就是 obj,所以这时候 bar 访问器里的 this 指向 obj,this.foo,实际就是 obj.foo。而 obj 并不是 proxy 对象 p,所以访问其 foo 属性并不会被拦截到。
那么如何也能触发到 foo 属性的拦截呢,这时候 Reflect 就派上用场了,有以下代码:
如上面代码所示,我们能正确地触发了 foo 属性的拦截,其实现的关键在于 Reflect.get 的第三个参数 receiver ,其作用就是改变 this 指向。
建议在 proxy 对象拦截器里的属性方法都通过 Reflex.*去操作。
资料
Last updated