Proxy详解
Proxy
Proxy
(代理),首先理解代理是什么意思,才能更便于了解Proxy
的作用。
Proxy
是一个代理,可以这么理解,就相当于它是一个快递代理点,快递会运输到该代理点,我们取件只需要去对应的代理点取件即可,代理点说快递没到就是没到,代理点说要出示证件就要出示证件。
Proxy
代理的是一个对象,该对象被代理后我们就不能直接访问,需要通过代理访问。我们想要获取对象内的某个值,代理说有就有,说没有就没有,代理返回的值长这样,这个值它就是这样,这就是代理,也可以将它理解成拦截。
创建Proxy
语法:let proxy = new Proxy(target,handler)
new Proxy()
用来生成Proxy
实例
target
参数表示要拦截的目标对象
handler
参数是一个对象,用来制定拦截行为
案例一:
let figure = {
name:'东方不败'
}
// 创建代理的方法,当通过代理访问目标对象时,此对象中的对应方法会执行
let handlers = {
get(target,prop){
return '我是代理,我说返回什么就是什么'
}
}
// 创建代理:代理的目标对象、代理的操作
let proxys = new Proxy(figure,handlers)
console.log(proxys.name); // 我是代理,我说返回什么就是什么
console.log(proxys.age); // 我是代理,我说返回什么就是什么
上面的例子就是利用Proxy
,通过Proxy
访问想要的值时,代理对取值进行拦截,返回给我们指定的信息,在这里,不论我们怎么取值,返回的都是我是代理,我说返回什么就是什么
。
案例二
let proxy2 = new Proxy({},{
get: function(target,propKey){
return 10;
}
})
proxy2.num = 20;
console.log(proxy2.num); // 10
console.log(proxy2.name); // 10
上面代码中,我们给Proxy
两个参数,第一个参数:要代理的目标对象(也就是{}
),第二个参数:配置对象(被代理对象的操作),在这里,配置对象有个get
方法,用来拦截目标对象属性的访问请求。
通过Proxy
访问属性的时候,get
方法将访问的这一层拦截了,这里拦截函数返回的10
,所以访问任何属性得到的都是10
。也就是说,给目标对象添加了属性值,但是在访问这一层被拦截了,任何访问都会返回拦截的这一层。
get
方法的两个参数分别是目标对象和所要访问的属性,这里没有做任何操作,直接返回10
,所以我们获取的值都是10
。
注意:如果想要Proxy
生效,必须操作Proxy
的实例,操作原对象是没有任何效果的。
案例三
如果Proxy
的第二个参数(配置对象)没有设置任何拦截,就等同于直接访问原对象。
let target = {}
let handler = {}
let proxy = new Proxy(target,handler)
proxy.time = 20;
console.log(proxy.time); // 20
//handler没有设置任何拦截效果,访问proxy就等于访问target
对象内是可以设置函数的,可以将Proxy
对象设置到object.proxy
属性,这样就可以在object
对象上调用
let object = { proxy : new Proxy(target,handler) }
// object.proxy调用
Proxy
实例也可以作为其他对象的原型对象
let proxy = new Proxy({},{
get : function(target,propKsy){
return 10
}
})
let obj = Object.create(proxy)
obj.time = 10;
console.log(obj);
上面这段代码中,proxy
是obj
对象的原型,obj
对象本身没有time
属性,所以根据原型链,会在proxy
对象上读取该对象。
Proxy的实例方法
get()
get()
用于拦截某个属性的读取操作,可以接受三个参数:
1、目标对象
2、属性名
3、Proxy
实例本身(操作指向的对象),该参数为可选参数。
let person = {
name : '张三'
}
let proxy = new Proxy(person,{
get:function(target,propKey){
// 判断对象上是否存在该属性名
if(propKey in target){
// 存在,返回该属性
return target[propKey]
} else {
// 不存在,抛出错误
throw new ReferenceError("属性名:" + propKey + "不存在")
}
}
})
console.log(proxy.name); // 张三
// console.log(proxy.age); // Uncaught ReferenceError: 属性名:age不存在
in
运算符:in
操作符用来判断某个属性属于
某个对象,可以是对象的直接属性,也可以是通过prototype
继承的属性。
上面这段代码表示,如果访问目标不存在则抛出错误。如果没有设置拦截函数,访问不存在的属性会返回undefined
。
set()
set()
方法用来拦截某个属性的赋值操作,接收四个参数,依次为:
1、目标对象
2、属性名
3、属性值
4、Proxy
实例本身(可选)
案例一
判断属性值是否大于200
,大于200
则报错,并且属性值必须是数字
let numHold = {
set: function(obj,prop,value){
if(prop === 'age'){
if(!Number(value)){
throw new TypeError('参数不是数字')
}
if(value > 200){
throw new RangeError('参数不能大于200')
}
}
// 如果条件满足则直接保存,将值赋值给该属性
obj[prop] = value
}
}
let persons = new Proxy({},numHold)
persons.age = 100
console.log(persons.age); // 100
// persons.age = 300 // 报错,参数不能大于200
// persons.age = '东方' // 报错,参数不是数字
上面代码中,设置了一个存值函数set
,对对象的age
属性进行赋值时,如果不满足age
属性的赋值要求则会抛出一个相应的错误,其实这种写法就相当于是在做数据验证。
利用
set
方法还可进行数据绑定,每当数据发生变化时,自动更新DOM
同一个拦截器可以设置多个拦截操作,有时候,我们会在对象内部设置内部属性,该内部属性以下划线_
开头,表示这些属性不应该被外部使用,结合get
和set
方法,可以做到防止这些内部属性被外部读写。
// 拦截方法
let handler = {
// 读取
get(target,key){
invariant(key,'get')
return target[key]
},
// 写入
set(target,key,value){
invariant(key,'set');
target[key] = value
return true
}
}
function invariant(key,action){
// 属性名的第一个字符是_则为私有方法,不允许外部读写
if(key[0] === '_'){
throw new Error(`${action}私有属性${key}无效`)
}
}
let target = {} // 当前对象
let proxy = new Proxy(target,handler)
// proxy._prop; // get私有属性_prop无效
// proxy._name = '东方不败' // set私有属性_name无效
apply()
apply
方法拦截函数的调用,call
和apply
操作,apply
方法可以接受三个参数,分别是:
1、目标对象
2、目标对象的上下文对象(this
)
3、目标对象的参数数组
案例一
let target = function(){return '东方不败'}
let handler = {
apply:function(){
return '我是Proxy'
}
}
let p = new Proxy(target,handler)
console.log(p()); // 我是Proxy
上面代码中,变量p
为Proxy
的实例,当它作为函数调用时,就会触发apply
方法拦截,返回apply
的返回结果
案例二
let twice = {
apply(target,ctx,args){
// console.log(Reflect.apply(...arguments)); // 3
return Reflect.apply(...arguments) * 2
}
}
function sum(left,right){
return left + right // 1 + 2
}
let proxy6 = new Proxy(sum,twice)
console.log(proxy6(1,2)); // 6
上面代码中,每当执行Proxy
函数(直接调用或call
和apply
调用),都会被apply
方法拦截。
console.log(proxy6.call(null,2,3)); // 10
has()
has()
方法用来拦截HasProperty
操作,即判断对象是否具有某个属性,该方法会生效。典型的操作就是in
运算符。
has()
方法接受两个参数:
1、目标对象
2、需要查询的属性名
// 使用has()方法隐藏某些属性,不被in运算符发现
let handler = {
has(target,key){
// key为传入的属性名,这里key[0]就是属性名的第一个字符
if(key[0] === '_'){
// 第一个字符是_则返回false
return false
}
return key in target
}
}
let target = { _prop:'foo',prop:'fuu' }
let proxy = new Proxy(target,handler)
console.log('_prop' in proxy); // false '_prop'属性不属于proxy对象
console.log('_prop' in target); // true '_prop'属性属于target对象
注意:
如果原对象不可配置或禁止扩展,这是has()
拦截就会报错
虽然for...in
循环也用到了了in
运算符,但是has()
拦截对for...in
循环不生效。
construct()
construct()
方法用于拦截new
命令,当对Proxy
实例使用new
命令的时候触发
construct()
接受三个参数:
1、目标对象
2、构造函数的参数数组
3、创建实例对象时,new
命令作用的构造函数(也就是下面例子中的p2
)
let con = {
construct:function(target,arg){
// target是一个函数(){}
// args是一个参数数组
// this : construct
console.log(this === con); // true
console.log('回调:'+arg.join(',')); // 回调:1,2
return { value : arg[0] * 10 } // construct返回的必须是一个对象,否则报错
// return 1 // 返回的不是对象,报错'construct' on proxy: trap returned non-object ('1')
}
}
let p = new Proxy(function(){},con)
console.log((new p(1,2).value)); // new p()触发construct拦截
注意:由于
construct()
拦截的是构造函数,所以它的目标对象必须是函数,否则会报错
注意:construct()
中的this
指向的是con
,而不是实例对象
deleteProperty()
deleteProperty
方法用于拦截delete
操作,如果这个方法抛出错误或返回false
,当前属性就无法被delete
命令删除,当对Proxy
实例使用delete
命令的时候触发
let del = {
deleteProperty(target,key){
invariant(key,'delete')
delete target[key] // 如invariant未抛出错误就证明是可以删除的,删除操作
return true // 抛出true
}
}
function invariant(key,action){
// 如果属性名的第一个字符是_说明是私有属性,抛出错误
if(key[0] === '_'){
throw new Error(`当前操作:${action}对于私有属性${key}无效`)
}
}
let target = {_prop:'foo'}
let proxy = new Proxy(target,del)
// console.log(delete proxy._prop); // 报错:当前操作:delete对于私有属性_prop无效
注意,目标对象自身的不可配置(
configurable
)的属性,不能被deleteProperty
方法删除,否则报错。
defineProperty()
defineProperty()
方法拦截Object.defineProperty()
操作
let h = {
defineProperty (target,key,desc){
// target 目标对象
// 目标对象的属性名
// desc目标对象的赋值
return false
// return target[key] = desc.value
}
}
let t = {}
let pr = new Proxy(t,h)
console.log(pr.foo = 'bar'); // 不会生效,被拦截
console.log(t); // {}
上面代码中,defineProperty()
方法内部没有任何操作,只返回false
,导致新添加的属性总是无效。
这里返回的false
只是用来提示操作失败,本身并不能阻止添加新属性。
Proxy.revocable()
Proxy.revocable()
方法返回一个可取消的 Proxy
实例。
let target = {}
let handler = {}
let {proxy, revoke} = Proxy.revocable(target , handler );
console.log(proxy.foo = 100); // 100
revoke() // 取消Proxy实例
console.log(proxy.foo); // Cannot perform 'get' on a proxy that has been revoked
Proxy.revocable()
返回一个对象,该对象内有proxy
属性和revoke
属性。
proxy
是Proxy
实例
revoke
是一个函数,用来取消Proxy
实例
上面代码中revoke
执行完后,取消了Proxy
实例,当再次访问Proxy
实例时会报错。
Proxy.revocable()
的一个使用场景:目标对象不允许直接访问,必须通过代理访问,一但访问结束,就是收回代理权,不允许再次访问。
this问题
虽然Proxy
可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在Proxy
代理的情况下,目标对象内部的this
关键字会指向Proxy
代理。
let target = {
m : function () {
console.log('proxy',this);
// m:() false
// Proxy {m: ƒ} true
console.log(this === proxy); // target === proxy : false
}
}
let handler = {}
let proxy = new Proxy(target,handler)
target.m(); // target === proxy : false
proxy.m(); // proxy === proxy : true
正常情况下,对象内函数的this
指向为对象本身,也就是上面代码的target
,但是上面代码中Proxy
代理了target
,一旦proxy
代理target
对象,那么target.m()
内部的this
就是指向proxy
,而不是target
。所以,虽然proxy
没有做任何拦截,但target.m()
和proxy.m()
会返回不一样的结果。
案例一
由于this
指向的变化,导致Proxy
无法代理目标对象
let _name = new WeakMap() // 将值保存在这里
class Person {
constructor(name){
_name.set(this,name) // set一个_name等于name
}
get name(){
return _name.get(this) // 返回_name的值
}
}
let jane = new Person('东方不败')
console.log(jane.name); // 东方不败
let proxy2 = new Proxy(jane,{})
console.log(proxy2.name); // undefined,这里的this指向的是Proxy,所以找不到值
上面代码中,目标对象东方不败
的name
属性,实际保存在外部WeakMap
对象_name
上面,通过this
键区分。由于通过proxy2.name
访问时,this
指向proxy2
,导致无法取到值,所以返回undefined
。
此外,有些原生对象的内部属性,只有通过正确的this
才能拿到,所以Proxy
也无法代理这些原生对象的属性
let t = new Date()
let h = {}
let p = new Proxy(t,h)
// console.log(p.getDate()); // 报错 this is not a Date object.
console.log(t.getDate()); // 8
上面代码中,getData()
方法只能在Date
对象实例上面拿到,如果this
不是Date
对象实例就会报错。这时,this
绑定原始对象,就可以解决这个问题。
let t2 = new Date('2023-01-01')
let h2 = {
get(target,prop){
if(prop === 'getDate'){
// 更改this指向,绑定原始对象
return target.getDate.bind(target)
}
return Reflect.get(target,prop);
}
}
let p2 = new Proxy(t2,h2)
p2.getDate // 8
bind()
方法主要就是将函数绑定到某个对象,bind()
会创建一个函数,函数体内的this
对象的值会被绑定到传入bind()
第一个参数的值。
另外,Proxy
拦截函数内部的this
,指向的是当前对象,对象内函数都是指向当前对象,其实就是对象内函数的this
指向问题。
let handler = {
get:function(target,key,receiver){
return 'hello,'+key
},
set:function(target,key,value){
console.log(this === handler ); // true
target[key] = value
return true
}
}
let proxy = new Proxy({},handler )
console.log(proxy.foo); // hello,foo
proxy.foo = 1 // 触发set方法
关于对象内函数的this
指向问题,请看另一篇:对象定义-解构-枚举属性遍历以及对象内函数
Proxy支持的拦截操作
1、get(target, propKey, receiver):拦截对象属性的读取,比如proxy.foo和proxy['foo']。
2、set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = v或proxy['foo'] = v,返回一个布尔值。
3、has(target, propKey):拦截propKey in proxy的操作,返回一个布尔值。
4、deleteProperty(target, propKey):拦截delete proxy[propKey]的操作,返回一个布尔值。
5、ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
6、getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
7、defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
8、preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
9、getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
10、isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
11、setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
12、apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
13、construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。
案例源码:https://gitee.com/wang_fan_w/es6-science-institute
如果觉得这篇文章对你有帮助,欢迎点亮一下star哟