详解实现属性的全面拦截

目标

在使用 Proxy 拦截属性时,一个常见挑战是:一次外部的属性访问,可能在内部触发对多个其他属性的读取。如何确保所有这些间接的属性访问都能被 Proxy 成功捕获并拦截,是我们需要解决的关键问题。

javascript 复制代码
const obj = {
    a:1,
    b:2,
    get c(){
        // 注意这里:this.a 和 this.b
        return this.a + this.b
    }
}

// TODO:实现属性的全面拦截

proxy.c // 分别打印 read c、read a、read b ,并输出 proxy 的 c 属性

初步设想

我们来看一段代码

javascript 复制代码
const obj = {
    a:1,
    b:2,
    get c(){
        // 注意这里:this.a 和 this.b
        return this.a + this.b
    }
}

const proxy = new Proxy(obj,{
    get:(target,key,receiver)=>{
        console.log('read',key)
        return target[key]
    }
})

proxy.c // 访问 proxy 的 c 属性

这段代码看起来挺简单,它的目标是:当有人想读 proxy 的任何属性时,我们都能先console.log('read', key),然后把读取操作委托给原始对象 obj

当你运行 proxy.c 的时候,你会发现控制台输出了: read c 然后得到结果 3

但是我们在访问 c 的时候,实际上还涉及了其他两个属性的访问,this.athis.b

原因,return target[key] 直接在原始对象 obj 上读取属性,导致 obj.cgetter 内部的 this.athis.b 也直接访问 obj 的属性,从而绕过了 proxy 的拦截。

不好懂的话没关系,我们看图 上图展示了target[key]proxy[key]的区别

由于target本身并没有拦截功能,所以当返回target[key]的时候,虽然读取了 this.athis.b,但并未触发陷阱

返回 proxy[key]

接着我们很容易就会想到,那就直接返回proxy[key]

javascript 复制代码
const obj = {
    a:1,
    b:2,
    get c(){
        return this.a + this.b
    }
}

const proxy = new Proxy(obj,{
    get:(target,key,receiver)=>{
        console.log('read',key)
        return proxy[key] // <- 区别在这
    }
})

proxy.c

这就大错特错了,会陷入无限递归的循环

原因在于你第一步调用proxy.c的时候,触发了陷阱,于是他返回了一个proxy[key]key == c),但是proxy[key]再次触发了陷阱,接着继续返回proxy[key]......

流程如图

使用反射

现在停下来思考一下总结一下,现在所面临的根本矛盾在于

target本身不具有拦截功能,而具有拦截功能的proxy对自身的调用会陷入无限递归的局面

那么我们如何实现一个,既具有拦截功能的同时,把递归的情况终止

我们看一下反射是如何解决这个问题的

javascript 复制代码
Reflect.get(target, propertyKey[, receiver])

-   target
    需要取值的目标对象

-   propertyKey
    需要获取的值的键值

-   receiver
    如果`target`对象中指定了`getter`,`receiver`则为`getter`调用时的`this`值。

如果我们指定了receiver会发生什么,如下图第三个流程

现在我们重写代码

javascript 复制代码
const obj = {
    a:1,
    b:2,
    get c(){
        return this.a + this.b
    }
}

const proxy = new Proxy(obj,{
    get:(target,key,receiver)=>{
        console.log('read',key)
        return Reflect.get(obj,key,proxy) // <- 区别在这
    }
})

proxy.c // 将会分别打印 read c、read a、read b ,并输出 proxy 的 c 属性

最终输出恰好如目标所言

结束了么?

细心的朋友可能会想到,我能不能用receiver来替代proxy呢?

javascript 复制代码
const obj = {
    a:1,
    b:2,
    get c(){
        return this.a + this.b
    }
}

const proxy = new Proxy(obj,{
    get:(target,key,receiver)=>{
        console.log('read',key)
        return Reflect.get(obj,key,receiver) // <- 区别在这
    }
})

proxy.c

我们来看看 MDN 怎么解释的

receiver表示 Proxy 或者继承 Proxy 的对象

现在我们来区分一下

  1. 如果填的是proxy,则所用到的代理拦截永远是proxy本身
  2. 如果填的是receiver,则所用到的代理拦截则是发属性访问的对象本身,也就是有可能是proxy,也有可能是继承自proxy的子类

总结

到这里,我们已经基本实现了一个简单的全面拦截属性的代理对象,但是至于用receiver还是proxy,这当然就看具体的业务需求了

相关推荐
夏幻灵6 小时前
HTML5里最常用的十大标签
前端·html·html5
Mr Xu_6 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝6 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions6 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发6 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法
程序员猫哥_6 小时前
HTML 生成网页工具推荐:从手写代码到 AI 自动生成网页的进化路径
前端·人工智能·html
龙飞057 小时前
Systemd -systemctl - journalctl 速查表:服务管理 + 日志排障
linux·运维·前端·chrome·systemctl·journalctl
我爱加班、、7 小时前
Websocket能携带token过去后端吗
前端·后端·websocket
AAA阿giao7 小时前
从零拆解一个 React + TypeScript 的 TodoList:模块化、数据流与工程实践
前端·react.js·ui·typescript·前端框架
杨超越luckly7 小时前
HTML应用指南:利用GET请求获取中国500强企业名单,揭秘企业增长、分化与转型的新常态
前端·数据库·html·可视化·中国500强