Proxy代理 与 反射

99 阅读4分钟

介绍

个人学习笔记

es6新增的代理与反射,提供了拦截并向基本操作嵌入额外行为的能力。在对目标对象的各种操作影响到目标对象之前可以在代理对象中对这些操作加以控制(Proxy类似于Object.defineProperty()

Proxy和Object.defineProperty的区别

Proxy和Object.defineProperty()都可以用来监视和修改对象的操作,但它们有一些区别:

  1. Proxy可以监听更多种类的操作,包括属性访问(get/set)、属性枚举(enumerate)、函数调用(apply/call)等,而Object.defineProperty()只能拦截属性访问和修改
  2. Proxy可以直接代理整个对象,而Object.defineProperty()只能处理单个属性。
  3. Proxy提供了更多的钩子方法(handler),以及更丰富的API,可以做到更细粒度、功能更强大的对象操作。同时,Proxy也比Object.defineProperty()的语法更简洁易懂。
  4. 由于Proxy是ES6中新增的特性,因此对于一些旧版本的浏览器可能不支持;而Object.defineProperty()已经存在较长时间,对于绝大部分主流浏览器都有良好的兼容性。

代理初体验

Proxy构造函数接收两个参数:目标对象处理程序对象

无论是目标对象修改属性,还是Proxy修改属性,都会在两个对象反映出来,因此两个对象访问的是同一个值

// 目标对象
const target = { id : 'target'}
// 处理程序对象
const handler = {}
// 创建一个空代理 
const proxy = new Proxy(target,handler)
console.log('proxy.id:',proxy.id)
console.log('target.id:',target.id)
console.log('=================================')
target.id = 'foo'
console.log('proxy.id:',proxy.id)
console.log('target.id:',target.id)
proxy.id = 'bar'
console.log('=================================')
console.log('proxy.id:',proxy.id)
console.log('target.id:',target.id)
// console.log(target instanceof proxy) 由于Proxy.prototype是undefined 所以不能使用instanceof(会报错)
console.log(target === proxy)

image.png

定义捕获器

使用代理的主要目的是可以定义捕获器(trap);捕获器就是在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象中可以定义零个或多个捕获器,可以直接或间接在代理对象上调用。(只有在代理对象中才会触发捕获器,在目标对象上仍然会执行正常的行为)

get()捕获器

通过代理对象执行获取属性值操作时,就会触发定义的get()捕获器

const target = {
    foo : 'bar'
}
const handler = {
    get(){
        return 'handler函数'
    }
}
// 以下这种通过proxy代理获取属性的值时都会触发get()捕获器
const proxy = new Proxy(target,handler)
console.log('target.foo:',target.foo)
console.log('proxy.foo:',proxy.foo)

console.log(`target['foo']:`,target['foo'])
console.log(`proxy['foo']:`,proxy['foo'])

console.log(`Object.create(target)['foo']:`,Object.create(target)['foo'])
console.log(`Object.create(proxy)['foo']:`,Object.create(proxy)['foo'])

console.log(`Object.create(target).foo:`,Object.create(target).foo)
console.log(`Object.create(proxy).foo:`,Object.create(proxy).foo)

image.png

捕获器参数和反射API

所有捕获器都可以访问相应的参数,基于这些参数可以重建捕获方法的原始行为

get()捕获器会接收到三种参数:目标对象,查询的属性,代理对象

const target = {
    foo : 'bar'
}
const handler = {
    get(trapTarget,property,receiver){
        console.log(trapTarget === target) // true
        console.log(property) // foo
        console.log(receiver === proxy) // true
    }
}
const proxy = new Proxy(target,handler)
proxy.foo

image.png

重构捕获方法

const target = {
    foo : 'bar'
}
const handler = {
    get(trapTarget,property,receiver){
        return trapTarget[property]
    }
}
const proxy = new Proxy(target,handler)
console.log(proxy.foo) // bar

反射API

并非所有捕获器行为都像get()那么简单,如法炮制的想法是不现实的。实际上并不需要手动重建捕获器原始行为,可以通过调用全局Reflect对象上的同名方法(封装了原始行为)来重建。

const target = {
    foo : 'bar'
}
const handler = {
    get(){
        return Reflect.get(...arguments)
    }
    // 简写
    get: Reflect.get
}
const proxy = new Proxy(target,handler)
console.log(proxy.foo) // bar

如果想创建一个可以捕获所有方法,然后将每个方法转发给对应反射API的空代理,那么可以这样简写

const target = {
    foo : 'bar'
}
const proxy = new Proxy(target,Reflect)
console.log(proxy.foo) // bar

反射API已经准备好了样板代码,在此基础上可以用最少的代码修改捕获的方法

const target = {
    foo : 'bar',
    baz : 'qux'
}
const handler = {
     get(trapTarget,property,receiver){
        let name = ''
        // 判断处理
        if(property === 'foo'){
            name = '我是帅哥'
        }
        return Reflect.get(...arguments) + name
    },
    set(){
        return Reflect.set(...arguments)
    }
}
const proxy = new Proxy(target,handler)
proxy.foo = '张三'
proxy.baz = '李四'
console.log(proxy.foo) 
console.log(target.foo)
console.log(target.baz)

image.png

每个捕获的方法都知道目标对象上下文、捕获函数前面,而捕获处理程序的行为必须遵循“捕获器不变式”。捕获器不变式因方法不同而异,但通常都会防止捕获器出现反常的行为

可撤销代理

有时候可能需要中断代理对象与目标对象的联系,但使用new Proxy()创建的普通代理是不可撤销的,它会在代理对象的生命周期中一直存在。

可以通过Proxy.revocable()方法来撤销代理对象与目标对象的联系。撤销代理的操作是不可逆的,且撤销函数是幂等的,无论调用多少次结果都是一样的

const target = {
    foo: 'bar'
}
const handler = {
    get(){
        return Reflect.get(...arguments) + '使用反射API'
    }
}
const {proxy , revoke} =  Proxy.revocable(target,handler)
console.log(proxy.foo)
console.log(target.foo)
revoke() // 撤销代理
console.log(proxy.foo) // 报错

image.png

代理的问题与不足

js红宝书273页