浅谈 ES6 中的 Proxy 用法
本文最后更新于:2022年7月8日 下午
WARNING
此处 Proxy 指的是ES6中的语法,而非 FireFox proxy 与 Chrome proxy WebExtensions。请注意,前者是标准语法,P大写;而后者是对应厂商的私有技术,p小写。
Proxy 介绍
MDN 中的描述:
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。其实通俗来讲,Proxy相当于在原生方法(例如get访问Object的中的属性或方法、set修改Object中的属性或方法、has拦截”in操作符判断属性或方法是否存在Object或其原型链上”等等)之上的一层捕获器,可以进行拦截、修改等多种操作。Proxy常见用法有很多,包括 运算符重载,对象模拟,简洁而灵活的API创建,对象更改事件 等等。
又比如在 Vue2 中是通过Object.defineProperty()来劫持各个属性的setter、getter来实现的双向绑定,而在 Vue3 中使用的正是Proxy来实现的双向绑定。
Proxy 用于修改某些操作的默认行为,也可以理解为在目标对象之前架设一层拦截,外部所有的访问都必须先通过这层拦截,因此提供了一种机制,可以对外部的访问进行过滤和修改。这个词的原理为代理,在这里可以表示由它来“代理”某些操作,译为“代理器”。
术语
handler包含捕捉器(trap)的占位符对象,可译为处理器对象。traps提供属性访问的方法。这类似于操作系统中捕获器的概念。target被Proxy代理虚拟化的对象。它常被作为代理的存储后端。根据目标验证关于对象不可扩展性或不可配置属性的不变量(保持不变的语义)。
语法
ES6 原生提供了
Proxy构造函数,用来生成Proxy实例。
target要使用 Proxy 包装的 目标对象Object(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。handler一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理的行为。
1 | |
注意: target 必须是 Object 类型,而不能是其他类型。
Proxy 对象的所有用法,都是上面的这种形式。不同的只是 handler 参数的写法。其中 new Proxy 用来生成 Proxy 实例,target 是表示所要拦截的对象(Object),handler 是用来定制拦截行为的对象。
例子
下面是 Proxy 最简单的例子是,这是一个有捕捉器的代理,一个 get 捕捉器,总是返回 42。
1 | |
结果是一个对象将为任何属性访问操作都返回“42”。 这包括 target.x,target['x'],Reflect.get(target, 'x') 等。
handler 对象的方法
handler 对象是一个容纳一批特定属性的占位符对象。它包含有 Proxy 的各个捕获器(trap)。
所有的捕捉器是可选的。如果没有定义某个捕捉器,那么就会保留源对象的默认行为。
getPrototypeOf():Object.getPrototypeOf 方法的捕捉器。setPrototypeOf():Object.setPrototypeOf 方法的捕捉器。isExtensible():Object.isExtensible 方法的捕捉器。preventExtensions():Object.preventExtensions 方法的捕捉器。getOwnPropertyDescriptor():Object.getOwnPropertyDescriptor 方法的捕捉器。defineProperty(): Object.defineProperty 方法的捕捉器。has():in 操作符的捕捉器。get():属性读取操作的捕捉器。set():属性设置操作的捕捉器。deleteProperty():delete 操作符的捕捉器。ownKeys():Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。apply():函数调用操作的捕捉器。construct():new 操作符的捕捉器。
此外,一些不标准的捕捉器已经被废弃并且移除了。
Proxy 例子
默认值/“零值”
在 Go 语言中,有零值(nil)的概念,零值是特定于类型的隐式默认结构值。其思想是提供类型安全的默认基元值,或者用gopher的话说,给结构一个有用的零值。
虽然不同的创建模式支持类似的功能,但 Javascript 无法用隐式初始值包装对象。Javascript 中未设置属性的默认值是 undefined。但 Proxy 可以改变这种情况。
1 | |
函数 withZeroValue 用来包装目标对象。 如果设置了属性,则返回属性值。 否则,它返回一个默认的 “零值” 。
从技术上讲,这种方法也不是隐含的,但如果我们扩展 withZeroValue,以 Boolean (false), Number (0), String (""), Object ({}),Array ([])等对应的零值,则可能是隐含的。
1 | |
此功能可能有用的一个地方是坐标系。 绘图库可以基于数据的形状自动支持2D和3D渲染。 不是创建两个单独的模型,而是始终将z默认为 0 而不是 undefined,这可能是有意义的。
负索引数组
在JS中获取数组中的最后一个元素方式通过写的很冗长且重复,也容易出错。 这就是为什么有一个TC39提案定义了一个便利属性
Array.lastItem来获取和设置最后一个元素。
其他语言,如 Python 和 Ruby,使用负组索引更容易访问最后面的元素。例如,可以简单地使用 arr[-1] 替代 arr[arr.length-1] 访问最后一个元素。
使用 Proxy 也可以在 Javascript 中使用负索引。
1 | |
target: 目标对象。property: 被获取的属性名。receiver:Proxy或者继承Proxy的对象
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 Proxy handlers 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。
与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。
点击可查看MDN解释
一个重要的注意事项是包含 get 的捕捉器(trap)字符串化所有属性。 对于数组访问,我们需要将属性名称强制转换为 Numbers,这样就可以使用加运算符(隐式转换)简洁地完成。
现在 arr[-1] 访问最后一个元素,arr[-2] 访问倒数第二个元素,以此类推。
1 | |
隐藏属性
众所周知 JS 没有私有属性。
Symbol最初是为了启用私有属性而引入的,但后来使用像Object.getOwnPropertySymbols这样的反射方法进行了淡化,这使得它们可以被公开发现。长期以来的惯例是将私有属性命名为前下划线_,有效地标记它们“不要访问”。Proxy提供了一种稍微更好的方法来屏蔽这些属性。
1 | |
hide 函数包装目标对象,并使得从 in 运算符和 Object.getOwnPropertyNames 等方法无法访问带有下划线的属性。
1 | |
更完整的实现还包括诸如 deleteProperty 和 defineProperty 之类的捕捉器。 除了闭包之外,这可能是最接近真正私有属性的方法,因为它们无法通过枚举,克隆,访问或修改来访问。
缓存
在客户端和服务器之间同步状态时遇到困难并不罕见。数据可能会随着时间的推移而发生变化,很难确切地知道何时重新同步的逻辑。
Proxy 启用了一种新方法:根据需要将对象包装为无效(和重新同步)属性。 所有访问属性的尝试都首先检查缓存策略,该策略决定返回当前在内存中的内容还是采取其他一些操作。
1 | |
这个函数过于简化了: 它使对象上的所有属性在一段时间后都无法访问。然而,将此方法扩展为根据每个属性设置生存时间(TTL),并在一定的持续时间或访问次数之后更新它并不困难。
1 | |
这个示例简单地使银行帐户余额在10秒后无法访问。
运算符重载
也许从语法上讲,最吸引人的 Proxy 用例是重载操作符的能力,比如使用 handler.has 的 in 操作符。
in 操作符用于检查指定的属性是否位于指定的对象或其原型链中。但它也是语法上最优雅的重载操作符。这个例子定义了一个连续range函数来比较数字。
1 | |
与Python不同,Python使用生成器与有限的整数序列进行比较,这种方法支持十进制比较,可以扩展为支持其他数值范围。
1 | |
尽管这个用例不能解决复杂的问题,但它确实提供了干净、可读和可重用的代码。
除了 in 运算符,我们还可以重载 delete 和 new。
总结
为什么要用 Proxy ?
就拿 Vue 举例来说,在 Vue2 中当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty 将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。 每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。
而这样一个个遍历对 data 进行修改显然是不高效的,所以在 Vue3 中使用了 Proxy 来进行数据劫持。
Vue3 使用 Proxy 来监控数据的变化。就本文以上所说,Proxy 是 ES6 中提供的功能,其作用为:用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。相对于 Object.defineProperty(),其有以下特点:
Proxy直接代理整个对象而非对象属性,这样只需做一层代理就可以监听同级结构下的所有属性变化,包括新增属性和删除属性。Proxy可以监听数组的变化。(在 Vue2 中是重写了数组中的各种方法才达到监听的目的,如push、pop、shift、unshift、splice、sort、reverse等)
在 Vue2 中,Object.defineProperty 会改变原始数据,而 Proxy 是创建对象的虚拟表示,并提供 set 、get 和 deleteProperty 等处理器,这些处理器可在访问或修改原始对象上的属性时进行拦截,有以下特点∶
- 不需用使用
Vue.$set或Vue.$delete触发响应式。 - 全方位的数组变化检测,消除了 Vue2 无效的边界情况。
- 支持
Map,Set,WeakMap和WeakSet。
Vue3 中 Proxy 实现的响应式原理与 Vue2 的实现原理相同,实现方式大同小异∶
get收集依赖;set、delete等触发依赖;- 对于集合类型,就是对集合对象的方法做一层包装:原方法执行后执行依赖相关的收集或触发逻辑;
THE END
Proxy 提供虚拟化接口来控制任何目标 Object 的行为。 这样做可以在简单性和实用性之间取得平衡,而不会牺牲兼容性。
也许使用 Proxy 的最令人信服的理由是,上面的许多示例只有几行,并且可以轻松组合以创建复杂的功能。