代理(Proxy)可以拦截并改变 JS 引擎的底层操作,如数据读取、属性定义、函数构造等一系列操作。ES6 通过对这些底层内置对象的代理陷阱和反射函数,让开发者能进一步接近 JS 引擎的能力。
一、代理与反射的基本概念
什么是代理和反射呢?
代理是用来替代另一个对象(target),JS 通过new Proxy()创建一个目标对象的代理,该代理与该目标对象表面上可以被当作同一个对象来对待。
当目标对象上的进行一些特定的底层操作时,代理允许你拦截这些操作并且覆写它,而这原本只是 JS 引擎的内部能力。
如果你对些代理&反射的概念比较困惑的话,可以直接看后面的应用示例,最后再重新看这些定义就会更清晰!
拦截行为使用了一个能够响应特定操作的函数( 被称为陷阱),每个代理陷阱对应一个反射(Reflect)方法。
ES6 的反射 API 以 Reflect 对象的形式出现,对象每个方法都与对应的陷阱函数同名,并且接收的参数也与之一致。以下是 Reflect 对象的一些方法:
代理陷阱
覆写的特性
方法
get
读取一个属性的值
Reflect.get()
set
写入一个属性
Reflect.set()
has
in 运算符
Reflect.has()
deleteProperty
delete 运算符
Reflect.deleteProperty()
getPrototypeOf
Object.getPrototypeOf()
Reflect.getPrototypeOf()
isExtensible
Object.isExtensible()
Reflect.isExtensible()
defineProperty
Object.defineProperty()
Reflect.defineProperty
apply
调用一个函数
Reflect.apply()
construct
使用 new 调用一个函数
Reflect.construct()
每个陷阱函数都可以重写 JS 对象的一个特定内置行为,允许你拦截并修改它。
综合来说,想要控制或改变JS的一些底层操作,可以先创建一个代理对象,在这个代理对象上挂载一些陷阱函数,陷阱函数里面有反射方法。通过接下来的应用示例可以更清晰的明白代理的过程。
二、开始一个简单的代理
当你使用 Proxy 构造器来创建一个代理时,需要传递两个参数:目标对象(target)以及一个处理器( handler),
先创建一个仅进行传递的代理如下:
// 目标对象 let target = {}; // 代理对象 let proxy = new Proxy(target, {}); proxy.name = "hello"; console.log(proxy.name); // "hello" console.log(target.name); // "hello" target.name = "world"; console.log(proxy.name); // "world" console.log(target.name); // "world
上例中的 proxy 代理对象将所有操作直接传递给 target 目标对象,代理对象 proxy 自身并没有存储该属性,它只是简单将值传递给 target 对象,proxy.name 与 target.name 的属性值总是相等,因为它们都指向 target.name。
此时代理陷阱的处理器为空对象,当然处理器可以定义了一个或多个陷阱函数。
2.1 set 验证对象属性的存储
假设你想要创建一个对象,并要求其属性值只能是数值,这就意味着该对象的每个新增属性
都要被验证,并且在属性值不为数值类型时应当抛出错误。
这时需要使用 set 陷阱函数来拦截传入的 value,该陷阱函数能接受四个参数:
trapTarget :将接收属性的对象( 即代理的目标对象)
key :需要写入的属性的键( 字符串类型或符号类型)
value :将被写入属性的值;
receiver :操作发生的对象( 通常是代理对象)
set 陷阱对应的反射方法和默认特性是Reflect.set(),和陷阱函数一样接受这四个参数,并会基于操作是否成功而返回相应的结果:
let targetObj = {}; let proxyObj = new Proxy(targetObj, { set: set }); /* 定义 set 陷阱函数 */ function set (trapTarget, key, value, receiver) { if (isNaN(value)) { throw new TypeError("Property " + key + " must be a number."); } return Reflect.set(trapTarget, key, value, receiver); } /* 测试 */ proxyObj.count = 123; console.log(proxyObj.count); // 123 console.log(targetObj.count); // 123 proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.
示例中set 陷阱函数成功拦截传入的 value 值,你可以尝试一下,如果注释或不return Reflect.set()会发生什么?,答案是拦截陷阱就不会有反射响应。
需要注意的是,直接给 targetObj 目标对象赋值时是不会触发 set 代理陷阱的,需要通过给代理对象赋值才会触发 set 代理陷阱与反射。
2.2 get 验证对象属性的读取
JS 非常有趣的特性之一,是读取不存在的属性时并不会抛出错误,而会把undefined当作该属性的值。