为什么要在Proxy里使用Reflect?

本文探讨了Vue3中使用Proxy替代Object.defineProperty的原因,着重解释了Proxy的receiver参数及其在继承场景中的作用。通过示例展示了Reflect在Proxy中的重要性,特别是在维护正确this上下文和优化性能方面。最后总结了必须使用Reflect的原因,包括保持默认行为、获取内置方法、避免错误处理和实现函数式编程。
摘要由CSDN通过智能技术生成

引言

大家都知道Vue3和Vue2之间很核心的改变就是使用Proxy代理来替换Object.defineProperty()。之前文章也提到了为什么Object.defineProperty不能监听到数组长度的变化?感兴趣的可以看下。
我们可以利用 Proxy 来实现对于对象的代理劫持操作。但是你也许并不清楚为什么要在Proxy里使用Reflect。
本文将通过几个例子来阐述它们之间的关系(假设你已经了解了什么是Proxy & Reflect),以及最后会讲述为什么必须要使用Reflect。

如何使用Proxy

const obj = {
  name: 'jiapandong',
};

const proxy = new Proxy(obj, {
  // get陷阱中target表示原对象 key表示访问的属性名
  get(target, key) {
    console.log('劫持你的数据访问' + key);
    return target[key]
  },
});

proxy.name // 劫持你的数据访问name -> jiapandong

我们通过 Proxy 创建了一个基于 obj 对象的代理,同时在 Proxy 中声明了一个 get 陷阱。
当访问我们访问 proxy.name 时实际触发了对应的 get 陷阱,它会执行 get 陷阱中的逻辑,同时会执行对应陷阱中的逻辑,最终返回对应的 target[key]也就是jiapandong

Proxy 中的 receiver

在查看MDN文档的时候会发现,get中还存在一个额外的参数receiver。那么它到底表示什么意思呢?有文章会把它理解为代理对象
这么理解是否正确呢?接下来,我们来举个简单的例子

const obj = {
  name: 'jiapandong',
};

const proxy = new Proxy(obj, {
  // get陷阱中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});

// 打印:true  jiapandong
proxy.name; 

上述的例子中,我们在 Proxy 实例对象的 get 陷阱上接收了 receiver 这个参数。
同时,我们在陷阱内部打印 console.log(receiver === proxy); 它会打印出 true ,表示这里 receiver 的确是和代理对象相等的。
所以 receiver 的确是可以表示代理对象。
再来,我们看另一个例子

const parent = {
  get value() {
    return 'wuyanzu';
  },
};

const proxy = new Proxy(parent, {
  // get陷阱中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
    console.log(receiver === proxy);
    return target[key];
  },
});

const obj = {
  name: 'jiapandong',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

// 打印: false
obj.value

我们可以看到,上述的代码同样我在 proxy 对象的 get 陷阱上打印了console.log(receiver === proxy); 结果却是 false
其实这就是 proxy 中 get 陷阱第三个 receiver 存在的意义:为了传递正确的调用者指向。下面我们验证一下:

...
const proxy = new Proxy(parent, {
  // get陷阱中target表示原对象 key表示访问的属性名
  get(target, key, receiver) {
-   console.log(receiver === proxy) // 打印: false
+   console.log(receiver === obj) // 打印: true
    return target[key];
  },
});
...

其实简单来说,get 陷阱中的 receiver 存在的意义就是为了正确的在陷阱中传递上下文。
我们可以清楚的看到上述的 receiver 代表的是继承与 Proxy 的对象,也就是 obj。
如上所得一个结论:Proxy 中 get 陷阱的 receiver 不仅仅代表的是 Proxy 代理对象本身,同时它会代表继承 Proxy 的那个对象。本质上来说它还是为了确保陷阱函数中调用者的正确的上下文访问,比如这里的 receiver 指向的是 obj 。

Reflect 中的 receiver

在清楚了 Proxy 中 get的 receiver 后,我们来聊聊 Reflect 反射 API 中 get 陷阱的 receiver。

const parent = {
  name: 'wuyanzu',
  get value() {
    return this.name;
  },
};

const handler = {
  get(target, key, receiver) {
    return Reflect.get(target, key);
    // 这里相当于 return target[key]
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'jiapandong',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

console.log(obj.value); // wuyanzu

我们分析下上面的代码:

  • 当我们调用 obj.value 时,由于 obj 本身不存在 value 属性。
  • 它继承的 proxy 对象中存在 value 的属性访问操作符,所以会发生屏蔽效果。
  • 此时会触发 proxy 上的 get value() 属性访问操作。
  • 同时由于访问了 proxy 上的 value 属性访问器,所以此时会触发 get 陷阱。
  • 进入陷阱时,target 为源对象也就是 parent ,key 为 value 。
  • 陷阱中返回 Reflect.get(target,key) 相当于 target[key]
  • 此时,不知不觉中 this 指向在 get 陷阱中被偷偷修改掉了!!
  • 原本调用方的 obj 在陷阱中被修改成为了对应的 target 也就是 parent 。
  • 自然而然打印出了对应的parent[value]也就是 wuyanzu 。

这显然不是我们期望的结果,当我访问 obj.value 时,我希望应该正确输出对应的自身上的 name 属性也就是所谓的 obj.value => jiapandong
那么,Relfect 中 get 陷阱的 receiver 就展现出它的作用了。

const parent = {
  name: 'wuyanzu',
  get value() {
    return this.name;
  },
};

const handler = {
  get(target, key, receiver) {
-   return Reflect.get(target, key);
+   return Reflect.get(target, key, receiver);
  },
};

const proxy = new Proxy(parent, handler);

const obj = {
  name: 'jiapandong',
};

// 设置obj继承与parent的代理对象proxy
Object.setPrototypeOf(obj, proxy);

console.log(obj.value); // jiapandong

上述代码原理其实非常简单:

  • 首先,之前我们提到过在 Proxy 中 get 陷阱的 receiver 不仅仅会表示代理对象本身同时也还有可能表示继承于代理对象的对象,具体需要区别与调用方。这里显然它是指向继承与代理对象的 obj 。
  • 其次,我们在 Reflect 中 get 陷阱中第三个参数传递了 Proxy 中的 receiver 也就是 obj 作为形参,它会修改调用时的 this 指向。

看到这里你应该已经明白 Relfect 中的 receiver 代表的含义是什么了:它可以修改属性访问中的 this 指向为传入的 receiver 对象

小结

Proxy因为要保证正确的this上下文指向,所以需要配合Reflect使用。

  • Proxy 中接受的 Receiver 形参表示代理对象本身或者继承与代理对象的对象。
  • Reflect 中传递的 Receiver 实参表示修改执行原始操作时的 this 指向。

最后:那么为什么必须要使用Reflect?

有细心的小伙伴看到这会产生一个想法:那既然是为了改变this的指向,为什么不可以直接使用target[key].call(receiver)呢?

1. Reflect和Proxy配对使用,提供对象语义的默认行为。

有如下代码:

const proxy = new Proxy(obj, {
  set(...args) {
    console.log('set', ...args);
    return Reflect.set(...args;);
  },
});

我们也可以写成:

 set(target, key, value) {
    console.log('set', target, key, value);
    target[key] = value;
 },

然而,想get/set这样的方法,我们比较容易知道其语义所对应的语法,但有些方法比如ownKeys,如果不用Reflect.ownkeys,就必须要自己去实现。Proxy上所有的方法在Reflect上都有一对一的实现。

最近发现Reflect.set有严重的性能问题,其实严格来说,这不能说Reflect有严重的性能问题,而是使用了反射,就失去了jit所能进行的许多优化(所谓fast path)。这在所有语言里都是类似的,只要用了反射,性能一定是差的。

2. 将 Object 对象一些内部的方法,放到 Reflect 对象上。比如 Object.defineProperty

现阶段这些方法存在于 object 和 Reflect 对象上,未来只存在于 Reflect 对象上。也就是说,从 Reflect 对象上可以拿到语言内部的方法。

3. 操作对象出现报错返回 false

比如:Object.defineProperty(obj,name,desc) 在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj,name,desc)则会返回 false,这样会更合理一些。

// 旧写法
try {
  Object.defineProperty(target, property, attributes);
} catch (err) {
  //failure
}

// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
  //success
} else {
  //failure
}

4. 让操作对象的编程变为函数式编程

老写法有的是命令式编程,比如下面这个例子

// 老写法
"assign" in Object; // true

// 新写法
Reflect.has(Object, "assign"); //true
你好!关于"vue3proxy结合reflect"的问题,我理解你可能想了解如何在Vue 3中使用Proxy与Reflect结合起来。Vue 3是一个流行的JavaScript框架,而Proxy和Reflect则是ES6中新增的特性。 在Vue 3中,可以使用Proxy对象来代理Vue实例,以便捕获对数据的访问和修改。Proxy对象可以拦截并处理对Vue实例的操作,比如读取、设置和删除属性等。而Reflect对象提供了一组用于操作对象的方法,比如Reflect.get()、Reflect.set()和Reflect.deleteProperty()等。 结合使用Proxy和Reflect可以提供更灵活的控制和监控能力。你可以通过在Vue实例上创建一个代理对象,来拦截对数据的访问和修改,并在代理处理器中使用Reflect方法来操作实际的数据。这样可以轻松地实现对数据的拦截、校验、代理等功能。 下面是一个简单的示例代码,展示了如何在Vue 3中使用Proxy和Reflect结合起来: ```javascript const data = { message: 'Hello, Vue!', }; const proxy = new Proxy(data, { get(target, key) { console.log('Getting ' + key); return Reflect.get(target, key); }, set(target, key, value) { console.log('Setting ' + key + ' to ' + value); return Reflect.set(target, key, value); }, }); // 创建Vue应用 const app = Vue.createApp({ data() { return proxy; // 使用代理对象 }, }); app.mount('#app'); ``` 在上面的代码中,我们创建了一个名为data的普通对象,并使用Proxy对象创建了一个代理对象proxy。在代理对象的get和set处理器中,我们分别使用Reflect.get和Reflect.set来操作实际的数据。这样,当我们通过Vue实例访问或修改数据时,会触发代理处理器,并通过Reflect方法操作实际的数据。 希望这个例子能帮助到你,如果还有其他问题,请随时提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值