JavaScript的Proxy代理
Proxy是什么
首先,我们要清楚,Proxy
是什么意思,这个单词翻译过来,就是 代理。
可以理解为,有一个很火的明星,开通了一个微博账号,这个账号非常活跃,回复粉丝、到处点赞之类的,但可能并不是真的由本人在维护的。
而是在背后有一个其他人 or 团队来运营,我们就可以称他们为代理人,因为他们发表的微博就代表了明星本人的意思。
P.S. 强行举例子,因为本人不追星,只是猜测可能会有这样的运营团队
这个代入到JavaScript
当中来,就可以理解为对对象
或者函数
的代理操作。
JavaScript中的Proxy
Proxy是ES6中提供的新的API,可以用来定义对象各种基本操作的自定义行为 (在文档中被称为traps,我觉得可以理解为一个针对对象各种行为的钩子),拿它可以做很多有意思的事情,在我们需要对一些对象的行为进行控制时将变得非常有效。
proxy代理,就是在目标对象的前面设置一个拦截层,外界在访问这个对象的时候,必须经过拦截层。
我们可以在拦截层做一些过滤或者是改写的操作。
const proxy = new Proxy({}, { get: () => { console.log("get"); }, set: () => { console.log("set") } }) proxy.count = 1; // set ++proxy.count; // get
Proxy接受两个参数,第一个参数是拦截目标对象;第二个参数是设置拦截的操作,所谓的拦截,一般都是有一些操作行为的,如果在拦截层没有设置任何操作的话,就会直接访问目标对象。
Proxy支持的拦截操作
get(target,propkey,receiver)
拦截对象的读取属性操作。
const proxy = new Proxy({ name: "duXin", count: 90 }, { get: () => { console.log("get"); return 100 }, }) console.log(proxy.name)
在访问目标对象的时候,无论是name属性还是count属性,都会返回100,因为在拦截层设置读取属性时都返回100。
set()
方法是设置属性值操作的捕获器。
const proxy_set = new Proxy({}, { set: function (target, prop, value, receiver) { target[prop] = value; console.log('property set: ' + prop + ' = ' + value); return true; } }) console.log("======proxy_set=======") console.log('name' in proxy_set); // false proxy_set.name = 'duXin'; // property set: name = duXin console.log('name' in proxy_set); // true
has()
对in操作符的代理方法。
const targetObj = { _secret: 'easily scared', eyeCount: 4 }; const proxy_has = new Proxy(targetObj, { has: (target, key) => { console.log("key==",key) if (key.includes('_')) { return false; } return key in target; } }) console.log('eyeCount' in targetObj); // true console.log('_secret' in proxy_has); // false
construct()
拦截new操作符,返回的是一个对象。
function proxy_construct_obj(disposition){ this.disposition = disposition; } const proxyConstruct = new Proxy(proxy_construct_obj, { construct: function (target, args) { return new target(...args); } }) console.log(new proxyConstruct("duXinYue").disposition)
apply()
拦截函数的调用。
语法:
var p = new Proxy(target, { apply: function(target, thisArg, argumentsList) { } });
target:目标对象,也就是函数
thisArg:被调用时的上下文对象
argumentsList:被调用时的参数,是一个类数组。
声明一个函数:
function sum(a, b) { return a + b; }
const proxy_apply = new Proxy(sum, { apply: (target,thisArg, argumentsList) => { console.log("target",target,thisArg, argumentsList) return target(argumentsList[0], argumentsList[1]) * 10; } }) console.log(proxy_apply(1,2)); //30
defineProperty()
拦截对象的 Object.defineProperty() 操作符。
这是语法:
defineProperty: function(target, property, descriptor) {}
target:目标对象,
property:被检索的属性名
descriptor:待定义或者修改的属性的描述符
defineProperty的返回值必须是Boolean值,表示对该属性操作是否成功。
var desc = { configurable: true, enumerable: true, value: 10 }; var defineProperty = new Proxy(desc, { defineProperty: function (target, prop, descriptor) { console.log('called: ' + prop); Reflect.defineProperty(target, prop, descriptor); } }); defineProperty.name = "908" console.log("obj", defineProperty); // { configurable: true, enumerable: true, value: 10, name: '908' }
拿Proxy来做些什么
因为在使用了Proxy
后,对象的行为基本上都是可控的,所以我们能拿来做一些之前实现起来比较复杂的事情。
在下边列出了几个简单的适用场景。
解决对象属性为undefined的问题
在一些层级比较深的对象属性获取中,如何处理undefined
一直是一个痛苦的过程,如果我们用Proxy
可以很好的兼容这种情况。
(() => { let target = {} let handlers = { get: (target, property) => { target[property] = (property in target) ? target[property] : {} if (typeof target[property] === 'object') { return new Proxy(target[property], handlers) } return target[property] } } let proxy = new Proxy(target, handlers) console.log('z' in proxy.x.y) // false (其实这一步已经针对`target`创建了一个x.y的属性) proxy.x.y.z = 'hello' console.log('z' in proxy.x.y) // true console.log(target.x.y.z) // hello })()
我们代理了get
,并在里边进行逻辑处理,如果我们要进行get
的值来自一个不存在的key
,则我们会在target
中创建对应个这个key
,然后返回一个针对这个key
的代理对象。
这样就能够保证我们的取值操作一定不会抛出can not get xxx from undefined
但是这会有一个小缺点,就是如果你确实要判断这个key
是否存在只能够通过in
操作符来判断,而不能够直接通过get
来判断。
普通函数与构造函数的兼容处理
如果我们提供了一个Class
对象给其他人,或者说一个ES5
版本的构造函数。
如果没有使用new
关键字来调用的话,Class
对象会直接抛出异常,而ES5
中的构造函数this
指向则会变为调用函数时的作用域。
我们可以使用apply
这个trap
来兼容这种情况:
class Test { constructor (a, b) { console.log('constructor', a, b) } } // Test(1, 2) // throw an error let proxyClass = new Proxy(Test, { apply (target, thisArg, argumentsList) { // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可 // throw new Error(`Function ${target.name} cannot be invoked without 'new'`) return new (target.bind(thisArg, ...argumentsList))() } }) proxyClass(1, 2) // constructor 1 2
我们使用了apply
来代理一些行为,在函数调用时会被触发,因为我们明确的知道,代理的是一个Class
或构造函数,所以我们直接在apply
中使用new
关键字来调用被代理的函数。
以及如果我们想要对函数进行限制,禁止使用new
关键字来调用,可以用另一个trap
:construct
function add (a, b) { return a + b } let proxy = new Proxy(add, { construct (target, argumentsList, newTarget) { throw new Error(`Function ${target.name} cannot be invoked with 'new'`) } }) proxy(1, 2) // 3 new proxy(1, 2) // throw an error
用Proxy来包装fetch
在前端发送请求,我们现在经常用到的应该就是fetch
了,一个原生提供的API。
我们可以用Proxy
来包装它,使其变得更易用。
let handlers = { get (target, property) { if (!target.init) { // 初始化对象 ['GET', 'POST'].forEach(method => { target[method] = (url, params = {}) => { return fetch(url, { headers: { 'content-type': 'application/json' }, mode: 'cors', credentials: 'same-origin', method, ...params }).then(response => response.json()) } }) } return target[property] } } let API = new Proxy({}, handlers) await API.GET('XXX') await API.POST('XXX', { body: JSON.stringify({name: 1}) })
对GET
、POST
进行了一层封装,可以直接通过.GET
这种方式来调用,并设置一些通用的参数。
实现一个简易的断言工具
写过测试的各位童鞋,应该都会知道断言这个东西console.assert
就是一个断言工具,接受两个参数,如果第一个为false
,则会将第二个参数作为Error message
抛出。
我们可以使用Proxy
来做一个直接赋值就能实现断言的工具。
let assert = new Proxy({}, { set (target, message, value) { if (!value) console.error(message) } }) assert['Isn\'t true'] = false // Error: Isn't true assert['Less than 18'] = 18 >= 19 // Error: Less than 18
统计函数调用次数
在做服务端时,我们可以用Proxy
代理一些函数,来统计一段时间内调用的次数。
在后期做性能分析时可能会能够用上:
function orginFunction () {} let proxyFunction = new Proxy(orginFunction, { apply (target, thisArg. argumentsList) { log(XXX) return target.apply(thisArg, argumentsList) } })
全部的traps
这里列出了handlers
所有可以定义的行为 (traps):
具体的可以查看MDN-Proxy
里边同样有一些例子
traps | description |
---|---|
get | 获取某个key 值 |
set | 设置某个key 值 |
has | 使用in 操作符判断某个key 是否存在 |
apply | 函数调用,仅在代理对象为function 时有效 |
ownKeys | 获取目标对象所有的key |
construct | 函数通过实例化调用,仅在代理对象为function 时有效 |
isExtensible | 判断对象是否可扩展,Object.isExtensible 的代理 |
deleteProperty | 删除一个property |
defineProperty | 定义一个新的property |
getPrototypeOf | 获取原型对象 |
setPrototypeOf | 设置原型对象 |
preventExtensions | 设置对象为不可扩展 |
getOwnPropertyDescriptor | 获取一个自有属性 (不会去原型链查找) 的属性描述 |