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);

在这里插入图片描述
上面这段代码中,proxyobj对象的原型,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

同一个拦截器可以设置多个拦截操作,有时候,我们会在对象内部设置内部属性,该内部属性以下划线_开头,表示这些属性不应该被外部使用,结合getset方法,可以做到防止这些内部属性被外部读写。

// 拦截方法
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方法拦截函数的调用,callapply操作,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

上面代码中,变量pProxy的实例,当它作为函数调用时,就会触发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函数(直接调用或callapply调用),都会被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属性。
proxyProxy实例
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哟

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐