浅谈 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
的最令人信服的理由是,上面的许多示例只有几行,并且可以轻松组合以创建复杂的功能。