关于前端: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);

下面这段代码中,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哟

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理